Android 遇坑之路及解决方案

2023-05-16

一.在状态栏之上弹自定义吐司

1.需求:UI设计师设计的原型图是在状态栏之上的位置弹一个自定义吐司,我们的应用内全部都是沉浸式状态栏,将状态栏隐藏掉了的。

2.解决方案:首先给toast设置marginTop为负的状态栏高度是无效的,然后查阅相关资料发现Toast是显示在Window之上的,查看Toast的源码发现实际起作用的是Toast的一个静态内部类TN,TN有一个成员变量mParams,实际上起作用的就是WindowManager.LayoutParams。
代码如下:

    private static class TN extends ITransientNotification.Stub {
        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

        private static final int SHOW = 0;
        private static final int HIDE = 1;
        private static final int CANCEL = 2;
        final Handler mHandler;

        int mGravity;
        int mX, mY;
        float mHorizontalMargin;
        float mVerticalMargin;


        View mView;
        View mNextView;
        int mDuration;

        WindowManager mWM;

        String mPackageName;

        static final long SHORT_DURATION_TIMEOUT = 4000;
        static final long LONG_DURATION_TIMEOUT = 7000;

        TN(String packageName, @Nullable Looper looper) {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

            mPackageName = packageName;

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }

        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

        public void cancel() {
            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }

        public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

        private void trySendAccessibilityEvent() {
            AccessibilityManager accessibilityManager =
                    AccessibilityManager.getInstance(mView.getContext());
            if (!accessibilityManager.isEnabled()) {
                return;
            }
            // treat toasts as notifications since they are used to
            // announce a transient piece of information to the user
            AccessibilityEvent event = AccessibilityEvent.obtain(
                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
            event.setClassName(getClass().getName());
            event.setPackageName(mView.getContext().getPackageName());
            mView.dispatchPopulateAccessibilityEvent(event);
            accessibilityManager.sendAccessibilityEvent(event);
        }

        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }

                mView = null;
            }
        }
    }

继续查找源码发现Toast有个成员函数是getWindowParams(),然后想拿toast调用它,仔细看,不对。这个函数是添加了@hide注解的,无语

 /**
     * Gets the LayoutParams for the Toast window.
     * @hide
     */
    public WindowManager.LayoutParams getWindowParams() {
        return mTN.mParams;
    }

既然正常途径拿不到,只有放出终极大招反射去获取这个方法了。
关键代码如下

//设置吐司可以在状态栏之上显示
        try {
            Class<?> aClass = Class.forName(name);
            Method method = aClass.getDeclaredMethod("getWindowParams");
            method.setAccessible(true);
            WindowManager.LayoutParams layoutParams1 = (WindowManager.LayoutParams) method.invoke(toast);
            layoutParams1.flags = layoutParams1.flags | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;

        } catch (Exception e) {
            e.printStackTrace();
        }

关键就是设置了一个Flag:
layoutParams1.flags | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
这个flag的含义是忽略状态栏的高度。

二.RecyclerView内部的一个bug

log日志如下

Fatal Exception: java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{57e90c1 position=17 id=17, oldPos=-1, pLpos:-1 no parent} android.support.v7.widget.RecyclerView{78f7e1c VFED..... ........ 55,102-1080,350 #7f09011b app:id/recyclerView}
       at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5610)
       at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5792)
       at android.support.v7.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:285)
       at android.support.v7.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:342)
       at android.support.v7.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:358)
       at android.support.v7.widget.GapWorker.prefetch(GapWorker.java:365)
       at android.support.v7.widget.GapWorker.run(GapWorker.java:396)
       at android.os.Handler.handleCallback(Handler.java:754)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at android.os.Looper.loop(Looper.java:163)
       at android.app.ActivityThread.main(ActivityThread.java:6401)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:791)

尝试了多种方式,最后同事帮找到了必现的操作,原因如下:应用中多处用到了RecyclerView,一个隐藏时Adapter的List做了clear操作,造成另一个RecyclerView滑动时会崩溃,躺着也中枪,后面修改了逻辑,改成隐藏时不重新初始化数据,只有显示时才重新init数据。

三.集合排序的bug,Collection,sorc()

log如下

Fatal Exception: java.lang.IllegalArgumentException: Comparison method violates its general contract!
       at java.util.TimSort.mergeLo(TimSort.java:777)
       at java.util.TimSort.mergeAt(TimSort.java:514)
       at java.util.TimSort.mergeCollapse(TimSort.java:439)
       at java.util.TimSort.sort(TimSort.java:245)
       at java.util.Arrays.sort(Arrays.java:1498)
       at java.util.ArrayList.sort(ArrayList.java:1470)
       at java.util.Collections.sort(Collections.java:201

解决方案:用Comparator接口对集合进行排序时,返回值不要直接返回p0.compareTo(p1),要考虑p0 == p1的情况。

四.把一个透明的Drawable处理成目标颜色的图片?

 /**
     *
     * @param drawable  图片
     * @param colors    颜色数组
     * @return          处理后的图片
     */
    public static Drawable tintDrawable(Drawable drawable, ColorStateList colors) {
        final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTintList(wrappedDrawable, colors);
        return wrappedDrawable;
    }

注意:这个操作是耗时操作,所以需要先在Background线程中做处理,然后再切换回UI线程显示图片。

五.View坐标不断变化的同时还要执行组合动画?

思路:逻辑分为两部分:

1.封装一个带动画的自定义View,View内处理组合动画的显示逻辑
2.View开启一个自定义的属性动画,在回调中不断设置View的位置。

自定义View的代码如下:

“`
/**
* Created by liuxu on 2018/6/20.
*/

public class AnimImageView extends View {
/**
* view的宽度
*/
private int width;
/**
* view的高度
*/
private int height;

private int realWidth; //绘制时真正用到的宽度
private int realHeight;//绘制时真正用到的高度
/**
 * 圆角半径
 */
private int circleAngle;
/**
 * 默认两圆圆心之间的距离=需要移动的距离
 */
private int default_two_circle_distance;
/**
 * 两圆圆心之间的距离
 */
private int two_circle_distance;
/**
 * 背景颜色
 */
private int bg_color = 0xffbc7d53;
/**
 * 按钮文字字符串
 */
private String buttonString = "确认完成";
/**
 * 动画执行时间
 */
private int duration = 1000;
/**
 * view向上移动距离
 */
private int move_distance = 50;
/**
 * 圆角矩形画笔
 */
private Paint paint;
/**
 * 文字画笔
 */
private Paint textPaint;
/**
 * 文字绘制所在矩形
 */
private Rect textRect = new Rect();

/**
 * 根据view的大小设置成矩形
 */
private RectF rectf = new RectF();

/**
 * 动画集
 */
private AnimatorSet animatorSet = new AnimatorSet();

/**
 * 圆到圆角矩形过度的动画   0.2s
 */
private ValueAnimator animator_circle_to_square;
/**
 * view上移的动画  动画的全部
 */
private ObjectAnimator animator_move_to_up;
/**
 * 渐变动画 透明度由1到0
 */
private ObjectAnimator animator_alpha;

public void setBg_color(int bg_color) {
    this.bg_color = bg_color;
    paint.setColor(bg_color);
    paint.setAlpha(126);
}

/**
 * 保持不变的动画  2000ms
 */
private ValueAnimator animator_stay;

private AnimationButtonListener animationButtonListener;

public void setAnimationButtonListener(AnimationButtonListener listener) {
    animationButtonListener = listener;
}

public AnimImageView(Context context) {
    this(context, null);
}

public void setButtonString(String buttonString) {
    this.buttonString = buttonString;
}

public AnimImageView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public AnimImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initPaint();

    animatorSet.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (animationButtonListener != null) {
                animationButtonListener.animationFinish();
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });
}


public void setCircleAngle(int circleAngle) {
    this.circleAngle = circleAngle;
}

private void initPaint() {

    paint = new Paint();
    paint.setStrokeWidth(4);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    paint.setColor(bg_color);

    textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    textPaint.setTextSize(MoliveKit.getPixels(13f));
    textPaint.setColor(Color.WHITE);
    textPaint.setTextAlign(Paint.Align.CENTER);
    textPaint.setAntiAlias(true);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    width = w;
    height = h;

}

public void setWidth(int width) {
    this.width = width;
}

public void setHeight(int height) {
    this.height = height;
}

public void setRealWidth(int realWidth) {
    this.realWidth = realWidth;
}

public void setRealHeight(int realHeight) {
    this.realHeight = realHeight;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    draw_oval_to_circle(canvas);
    //绘制文字
    drawText(canvas);
}


/**
 * 绘制圆形变成圆角矩形
 *
 * @param canvas 画布
 */
private void draw_oval_to_circle(Canvas canvas) {

    rectf.left = two_circle_distance;
    rectf.top = (height - realHeight) / 2;
    rectf.right = realWidth - two_circle_distance;
    rectf.bottom = height - (height - realHeight) / 2;


    //画圆角矩形
    canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);


}

/**
 * 绘制文字
 *
 * @param canvas 画布
 */
private void drawText(Canvas canvas) {
    textRect.left = 0;
    textRect.top = 0;
    textRect.right = width;
    textRect.bottom = height;
    Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
    int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
    //文字绘制到整个布局的中心位置
    canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint);
}

/**
 * 初始化所有动画
 */
private void initAnimation() {
    set_circle_to_square();
    set_animator_stay();
    set_animator_alpha();
    set_move_to_up();

    animatorSet.play(animator_circle_to_square)
            .with(animator_stay)
            .with(animator_alpha)
            .with(animator_move_to_up);

}

/**
 * 上升动画
 */
private void set_move_to_up() {
    final float curTranslationY = this.getTranslationY();
    animator_move_to_up = ObjectAnimator.ofFloat(this, "translationY", curTranslationY, curTranslationY - move_distance);
    animator_move_to_up.setDuration(3000);
    animator_move_to_up.setInterpolator(new AccelerateDecelerateInterpolator());
    animator_move_to_up.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
        }
    });

}

/**
 * 透明度变化动画
 */
private void set_animator_alpha() {

    animator_alpha = ObjectAnimator.ofFloat(this, "alpha", 1f, 0);
    animator_alpha.setDuration(800);
    animator_alpha.setStartDelay(2200);
    animator_alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            invalidate();
        }
    });

}

/**
 * 保持动画
 */
private void set_animator_stay() {
    animator_stay = ValueAnimator.ofInt(0, 0);
    animator_stay.setDuration(2000);
    animator_stay.setStartDelay(200);
    animator_stay.addUpdateListener(animation -> {
        two_circle_distance = (int) animation.getAnimatedValue();
        invalidate();
    });

}

/**
 * 拉伸动画
 */
private void set_circle_to_square() {
    animator_circle_to_square = ValueAnimator.ofInt(default_two_circle_distance, 0);
    animator_circle_to_square.setDuration(200);
    animator_circle_to_square.addUpdateListener(animation -> {
        two_circle_distance = (int) animation.getAnimatedValue();
        int alpha = 255 - (two_circle_distance * 255) / default_two_circle_distance;

        textPaint.setAlpha(alpha);
        invalidate();
    });

}

/**
 * 接口回调
 */
public interface AnimationButtonListener {
    /**
     * 动画完成回调
     */
    void animationFinish();
}

/**
 * 启动动画
 */
public void start() {
    //一些必要参数的初始化
    initAnimation();
    animatorSet.start();
}

public int getWidthForTextSize() {
    Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mTextPaint.setTextSize(15f);
    mTextPaint.setColor(Color.WHITE);
    // Define the string.
    // Measure the width of the text string.
    int textWidth = (int) mTextPaint.measureText(buttonString);
    return textWidth;
}```

六.Kotlin协程

1.协程是为了解决什么问题?
协程是为了解决各种异步回调带来的代码臃肿。另外协程的根本目的是为了提高系统对资源的利用率。

2.如何使用
需要在build中引入两个包。kotlinx-coroutines-core和kotlinx-coroutines-android
代码如下:


launch {
//运行在工作线程
do some 耗时操作
launch(UI) {
//运行在主线程
do some 更新UI操作
}
}

七.总结

比如kotlin目前也是在不断的学习中,很多新的API和功能在尝试使用,后续会继续做一些专题的总结,比如性能优化或者是代码重构,某些特殊功能点等等。笔者水平有限,如有错误请指正,谢谢。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android 遇坑之路及解决方案 的相关文章

随机推荐

  • ubuntu20环境下使用DevStack安装Openstack-Wallaby(单节点、多节点)

    文章目录 一 单节点部署1 环境准备1 1 镜像源1 2 pip源1 3 安装依赖包 2 OpenStack安装 wallaby2 1 添加 96 stack 96 用户2 2 设置代理2 3 下载devstack xff0c 使用 96
  • 【操作系统】页面置换算法

    页面置换算法 在进程运行过程中 xff0c 若需要访问的物理块不在内存中 xff0c 就需要通过一定的方式来将页面载入内存 xff0c 而此时内存很可能已无空闲空间 xff0c 因此就需要一定的算法来选择内存中要被置换的页面 xff0c 这
  • 前端 好看实用的颜色大全(16进制)

  • 解决linux写入ntfs盘时报错:只读文件系统

    2018 10 28 更新 可能因为在挂载wimdows盘后 xff0c 强制关机造成的 xff0c 可使用 sudo ntfsfix dev 来修复 其中 xff0c 为具体哪个盘 xff0c 例如sudo ntfsfix dev sda
  • 【计算机网络】TCP IP通信处理过程

    1 数据包首部 每个分层中都会对所发送的数据附加一个首部 xff0c 其中包含了该层必要的信息 xff0c 如发送端地址 接收端地址以及协议等相关信息 2 发送数据包 1 xff09 应用程序处理 进行编码处理 xff08 相当于表示层功能
  • 【高性能定时器】 时间轮

    时间轮 简述 顾名思义 xff0c 时间轮就像一个轮子 xff0c 在转动的时候外界会指向轮子不同的区域 xff0c 该区域就可以被使用 因此只要将不同时间的定时器按照一定的方法散列到时间轮的不同槽 xff08 即时间轮划分的区域 xff0
  • 系统调用中断(EINTR)与SIGCHLD信号的处理

    一 被中断的系统调用 EINTR 的理解 1 慢系统调用是 xff1f 2 慢系统调用的类别3 EINTR产生的原因5 一般处理方法 二 SIGCHLD信号的处理 1 SIGCHLD信号的产生2 SIGCHLD信号的处理3 不处理SIGCH
  • 定时器与超时的设置

    一 相关时间函数 1 gettimeofday 2 time 3 clock 二 间隔定时器 1 setitimerval 2 getitimerval 3 实时定时器的使用 三 为阻塞操作设置超时 1 alarm 2 给read 设置读超
  • 解决tomcat启动时,端口被占用问题

    有时候我们启动tomcat的时候 xff0c 会提示端口被占用 xff0c 我们可以用下面的方法解决这个问题 1 进入cmd 2 输入netstat ano findstr 8080 xff08 注 xff1a 8080为被占用的端口名 x
  • Maven实战(六)--- dependencies与dependencyManagement的区别

    在上一个项目中遇到一些 jar 包冲突的问题 xff0c 之后还有很多人分不清楚 dependencies 与 dependencyManagement 的区别 xff0c 本篇文章将这些区别总结下来 1 DepencyManagement
  • VSFTP服务器使用retrieveFileStream返回null的问题

    VSFTP服务器使用retrieveFileStream返回null的问题 最近在使用vsftp在文件存储服务 xff0c 发现使用retrieveFileStream获取文件流的时候 xff0c 怎么获取都是空的 xff0c 网上有说返回
  • Android常用的一些make命令

    1 make jX X表示数字 xff0c 这个命令将编译Android系统并生成镜像 xff0c XX表示可以使用到的CPU核数 xff0c 这在配置好的电脑上特别有用 xff0c 公司的16核ubuntu服务器执行make j16只要不
  • 建造者模式详解

    建造者模式 建造者模式 xff08 Bulider Pattern xff09 是将一个复杂对象的构建过程与它的实现表示分离 xff0c 使得同样的构建过程可以创建不同的表示 xff0c 属于创建型模式 使用创建者模式对于用户而言只需要制定
  • LAMP架构简述

    目录 一 LAMP架构简述 二 各组件作用 三 构建LAMP平台 3 1编译安装Apache httpd服务 3 1 1 关闭防火墙 xff0c 将安装Apache所需软件包转到 opt目录下 3 1 2 安装环境依赖包 3 1 3 设置安
  • SLF4J源码分析

    介绍 官网 xff1a http www slf4j org github xff1a https github com qos ch slf4j SLF4J xff08 Simple Logging Facade for Java xff
  • ssh指定密码登陆远程服务器

    1 ssh远程登录 ssh登录远程服务器 xff0c 一般都通过ssh key方式免密码登陆 xff1b 也可以指定用户名 密码来登陆远程服务器 xff1b 如下 xff1a ssh IP l user p port 或者 ssh user
  • U盘制作ubuntu18.04.6系统安装盘

    U盘制作ubuntu18 04 6系统安装盘 1 1 下载镜像文件 ubuntu 18 04 6 desktop amd64 iso 2 下载u盘制作工具rufus3 15 3 运行rufus3 1 制作unbutun18 04启动U盘 3
  • Gradle全版本资源下载

    使用说明 1 xff0c 官网下载地址https services gradle org distributions 不同版本更改版本号即可 xff0c 官网下载不成功的 xff0c 嫌官网下载速度慢的 xff0c 可以直接在这里下载 2
  • .sh文件无法运行--权限问题

    下载了 sh后缀名脚本 结果双击安装文件时是用gedit打开 试着从终端打开 xff0c 提示 xff1a command not found 最后发现原来是文件的打开权限没有钩选 允许以程序执行文件 在图形界面下无法修改权限 xff0c
  • Android 遇坑之路及解决方案

    一 在状态栏之上弹自定义吐司 1 需求 xff1a UI设计师设计的原型图是在状态栏之上的位置弹一个自定义吐司 xff0c 我们的应用内全部都是沉浸式状态栏 xff0c 将状态栏隐藏掉了的 2 解决方案 xff1a 首先给toast设置ma