• [技术干货] Android Handler 机制原理详解-转载
    一、 概述Handler 机制是 Android 系统中实现**线程间通信(Inter-thread Communication)**的核心机制,尤其广泛用于将子线程中的任务结果传递回主线程(UI线程)进行更新操作。其底层基于 消息队列(MessageQueue) 和 循环器(Looper) 构建,形成一个典型的生产者-消费者模型。该机制的核心目标是:实现跨线程的消息传递避免多线程并发修改 UI提供延迟执行和定时任务的能力支持异步任务调度与回调处理二、 核心组件1. Handler(处理器)Handler 是开发者最常接触的类,负责:发送消息:sendMessage()、post() 等方法处理消息:通过重写 handleMessage(Message msg) 或设置 Callback与特定线程的 Looper 和 MessageQueue 绑定⚠️ 每个 Handler 必须关联一个 Looper,否则会抛出异常。// 示例:创建 Handler 并绑定当前线程的 LooperHandler handler = new Handler(Looper.myLooper()) {    @Override    public void handleMessage(@NonNull Message msg) {        // 在指定线程中处理消息    }};2. MessageQueue(消息队列)MessageQueue 是一个按时间排序的消息队列,内部使用单向链表结构存储待处理的消息。主要职责:存储由 Handler 发送的 Message按 Message.when(执行时间戳)排序插入提供 next() 方法供 Looper 取出下一条可执行消息支持阻塞等待(无消息时休眠,有消息时唤醒)💡 MessageQueue 并非传统 FIFO 队列,而是根据执行时间排序的优先队列。3. Looper(循环器)Looper 是消息循环的驱动者,每个线程最多只能有一个 Looper。关键功能:调用 Looper.prepare() 初始化当前线程的 Looper调用 Looper.loop() 启动无限循环,不断从 MessageQueue 取出消息并分发使用 ThreadLocal 保证线程局部性// 主线程中系统自动调用Looper.prepareMainLooper(); // 初始化主线程 Looper...Looper.loop(); // 开始循环4. Message(消息)Message 是消息的载体,包含以下核心字段:字段    说明what    用户自定义消息类型arg1, arg2    整型参数,用于传递简单数据obj    任意对象(注意内存泄漏风险)target    目标 Handler,由 Handler.sendMessage() 自动设置callback    如果是 post(Runnable),则 Runnable 封装为 callbackwhen    消息应被执行的时间戳(毫秒)next    指向链表中的下一个 Message推荐使用 Message.obtain() 获取实例,避免频繁创建对象。三、工作原理1. 整体流程图2. 详细执行过程1. 初始化 Looper(子线程示例)在非主线程中若要使用 Handler,必须手动创建 Looper。class WorkerThread extends Thread {    public Handler handler;    @Override    public void run() {        Looper.prepare(); // 创建当前线程的 Looper 和 MessageQueue        handler = new Handler(Looper.myLooper()) {            @Override            public void handleMessage(@NonNull Message msg) {                switch (msg.what) {                    case 1:                        Log.d("Handler", "收到消息: " + msg.obj);                        break;                }            }        };        Looper.loop(); // 开始无限循环读取消息    }    public void quit() {        if (handler != null) {            handler.getLooper().quit(); // 安全退出循环        }    }}📌 注意:Looper.prepare() 只能调用一次,否则抛出 RuntimeExceptionLooper.loop() 是阻塞方法,直到调用 quit() 才退出2. 消息发送机制Handler 提供多种发送消息的方式:方法    说明sendEmptyMessage(int what)    发送空消息sendMessage(Message msg)    发送自定义消息post(Runnable r)    发送 Runnable 任务sendEmptyMessageDelayed(what, delay)    延迟发送postDelayed(Runnable r, long delayMillis)    延迟执行 Runnable底层均调用 enqueueMessage() 将消息插入 MessageQueue。// 示例:多种发送方式handler.sendEmptyMessage(1);Message msg = Message.obtain();msg.what = 2;msg.obj = "Hello";handler.sendMessage(msg);handler.post(() -> {    Log.d("Handler", "Runnable 执行");});handler.postDelayed(() -> {    Log.d("Handler", "1秒后执行");}, 1000);3. 消息处理流程Handler.dispatchMessage() 是消息分发的核心逻辑:public void dispatchMessage(@NonNull Message msg) {    if (msg.callback != null) {        // 优先处理 post(Runnable) 类型的消息        handleCallback(msg);    } else {        if (mCallback != null) {            // 其次交给 Handler 的 Callback 处理            if (mCallback.handleMessage(msg)) {                return;            }        }        // 最终调用 handleMessage        handleMessage(msg);    }}private void handleCallback(Message message) {    message.callback.run(); // 直接执行 Runnable}执行顺序:Runnable > Callback > handleMessage四、源码分析(精简版)1. Looper 核心实现public final class Looper {    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();    final MessageQueue mQueue;    final Thread mThread;    private Looper(boolean quitAllowed) {        mQueue = new MessageQueue(quitAllowed);        mThread = Thread.currentThread();    }    // 准备 Looper    public static void prepare() {        prepare(true);    }    private static void prepare(boolean quitAllowed) {        if (sThreadLocal.get() != null) {            throw new RuntimeException("Only one Looper may be created per thread");        }        sThreadLocal.set(new Looper(quitAllowed));    }    // 获取当前线程的 Looper    public static @Nullable Looper myLooper() {        return sThreadLocal.get();    }    // 消息循环主方法    public static void loop() {        final Looper me = myLooper();        if (me == null) {            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");        }        final MessageQueue queue = me.mQueue;        for (;;) {            Message msg = queue.next(); // 可能阻塞            if (msg == null) return; // 退出循环            // 分发消息            try {                msg.target.dispatchMessage(msg);            } finally {                // 空实现,预留钩子            }            msg.recycleUnchecked(); // 回收消息        }    }}🔍 关键点:ThreadLocal 保证每个线程独享一个 Looperloop() 是死循环,通过 queue.next() 阻塞等待消息msg.target 即发送该消息的 Handler2. MessageQueue 关键方法enqueueMessage() —— 入队boolean enqueueMessage(Message msg, long when) {    synchronized (this) {        if (msg.target == null) {            throw new IllegalArgumentException("Message must have a target.");        }        final long now = SystemClock.uptimeMillis();        msg.when = when;        Message p = mMessages; // 链表头节点        if (p == null || when == 0 || when < p.when) {            // 插入头部            msg.next = p;            mMessages = msg;        } else {            // 按时间顺序插入中间            Message prev;            for (;;) {                prev = p;                p = p.next;                if (p == null || when < p.when) {                    break;                }            }            msg.next = p;            prev.next = msg;        }        // 唤醒消息队列(如果之前处于等待状态)        notify();    }    return true;}next() —— 出队(核心阻塞方法)Message next() {    final long ptr = mPtr;    if (ptr == 0) return null; // 已退出    long pendingIdleHandlerCount = -1;    long nextPollTimeoutMillis = 0;    for (;;) {        // 阻塞等待,timeout 为 0 表示无限等待        nativePollOnce(ptr, nextPollTimeoutMillis);        synchronized (this) {            final long now = SystemClock.uptimeMillis();            Message prevMsg = null;            Message msg = mMessages;            // 同步屏障处理:跳过同步消息,只处理异步消息            if (msg != null && msg.target == null) {                do {                    prevMsg = msg;                    msg = msg.next;                } while (msg != null && !msg.isAsynchronous());            }            if (msg != null) {                if (now < msg.when) {                    // 未到执行时间,计算等待时间                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);                } else {                    // 可执行,取出消息                    if (prevMsg != null) {                        prevMsg.next = msg.next;                    } else {                        mMessages = msg.next;                    }                    msg.next = null;                    msg.markInUse();                    return msg;                }            } else {                // 无消息,无限等待                nextPollTimeoutMillis = -1;            }            // 处理 IdleHandler(空闲时执行)            if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {                pendingIdleHandlerCount = mIdleHandlers.size();            }            if (pendingIdleHandlerCount > 0) {                // 执行空闲任务            }        }    }}五、高级特性1. 同步屏障(Sync Barrier)用于优先处理异步消息(如 UI 绘制),屏蔽同步消息。应用场景:ViewRootImpl 请求重绘时插入屏障,确保 Choreographer 的 VSYNC 消息优先执行// 插入同步屏障int token = mHandler.getLooper().getQueue().postSyncBarrier();// 移除屏障mHandler.getLooper().getQueue().removeSyncBarrier(token);⚠️ 滥用可能导致同步消息饥饿,不建议普通应用使用。2. 空闲处理器(IdleHandler)当 MessageQueue 没有消息需要处理时,会回调 IdleHandler,可用于执行低优先级任务。Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {    @Override    public boolean queueIdle() {        Log.d("IdleHandler", "消息队列空闲,执行预加载");        // 返回 true:保持注册;false:执行一次后移除        return false;    }});典型用途:延迟初始化组件预加载数据执行非关键任务以提升流畅度3. 异步消息(Asynchronous Message)避免被同步屏障阻塞,适用于高优先级任务。Message msg = Message.obtain(handler, runnable);msg.setAsynchronous(true);handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + 1000);Handler.createAsync() 可创建专门处理异步消息的 Handler六、内存优化机制1. Message 对象池(对象复用)为减少频繁创建/销毁对象带来的 GC 压力,Message 内部维护了一个对象池。public final class Message implements Parcelable {    private static final int MAX_POOL_SIZE = 50;    private static Message sPool;    private static int sPoolSize = 0;    private static final Object sPoolSync = new Object();    public static Message obtain() {        synchronized (sPoolSync) {            if (sPool != null) {                Message m = sPool;                sPool = m.next;                m.next = null;                m.flags = 0;                sPoolSize--;                return m;            }        }        return new Message();    }    public void recycle() {        if (isInUse()) return;        recycleUnchecked();    }    void recycleUnchecked() {        flags = FLAG_IN_USE;        what = 0;        arg1 = 0;        arg2 = 0;        obj = null;        target = null;        callback = null;        data = null;        when = 0;        synchronized (sPoolSync) {            if (sPoolSize < MAX_POOL_SIZE) {                next = sPool;                sPool = this;                sPoolSize++;            }        }    }}建议始终使用 Message.obtain() 而非 new Message()七、 使用注意事项1. 避免内存泄漏常见场景:非静态内部类 Handler 持有外部 Activity 引用,导致无法回收。解决方案:静态内部类 + 弱引用private static class SafeHandler extends Handler {    private final WeakReference<MainActivity> mActivity;    public SafeHandler(MainActivity activity) {        mActivity = new WeakReference<>(activity);    }    @Override    public void handleMessage(@NonNull Message msg) {        MainActivity activity = mActivity.get();        if (activity == null || activity.isFinishing() || activity.isDestroyed()) {            return;        }        switch (msg.what) {            case 1:                activity.updateUI((String) msg.obj);                break;        }    }}或使用 WeakHandler(第三方库)// 如 Square 的 LeakCanary 推荐方式WeakHandler weakHandler = new WeakHandler(this, new Handler.Callback() {    @Override    public boolean handleMessage(@NonNull Message msg) {        // 安全处理        return true;    }});运行本项目java运行123456782. 正确退出 Looper在子线程中使用 Looper 后,应在适当时机退出,避免资源浪费。// 安全退出:处理完已到时的消息再退出handler.getLooper().quitSafely();// 立即退出:丢弃所有未处理消息handler.getLooper().quit();建议在 Thread 的 onDestroy 或 quit() 方法中调用3. 主线程 vs 子线程线程类型    Looper 是否自动创建    使用方式主线程(UI线程)    是(系统自动调用 Looper.prepareMainLooper())    直接创建 Handler子线程    否    必须手动调用 Looper.prepare() 和 Looper.loop()八、常见问题与使用建议问题    解决方案Can't create handler inside thread that has not called Looper.prepare()    在子线程中先调用 Looper.prepare()内存泄漏    使用静态 Handler + WeakReference消息延迟不准    系统休眠或高负载可能导致延迟,不保证精确时间大量消息堆积    控制消息频率,避免 OOM使用 postDelayed 实现轮询    不推荐,应使用 AlarmManager 或 WorkManager 替代九、总结Android 的 Handler 机制是一个精巧的线程通信架构,其设计体现了以下几个关键思想:组件    角色    设计亮点Handler    消息生产者与消费者    提供易用的 APIMessageQueue    消息存储与调度    单链表 + 时间排序 + 阻塞唤醒机制Looper    消息循环驱动    ThreadLocal + 无限循环Message    消息载体    对象池复用 + 多种数据承载方式1.核心优势线程安全:保证 UI 更新在主线程完成异步解耦:任务可在任意线程发起,在指定线程执行延迟执行:支持定时任务和延迟操作内存高效:Message 对象池减少 GC 压力扩展性强:支持同步屏障、空闲处理器等高级特性2.应用场景子线程更新 UI延迟操作(如启动页倒计时)定时任务(需结合 removeCallbacks 防止泄漏)线程间状态同步————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/csdn_silent/article/details/153244375
  • [技术干货] adb 命令导出apk
    在 Android 调试中,可以使用 adb 命令导出设备上已安装应用的 APK 文件。以下是详细步骤和常见用法:方法 1:通过 pm path 获取 APK 路径并拉取步骤 1:查找应用的包名adb shell pm list packages | grep "关键词" 示例(查找微信包名):adb shell pm list packages | grep "wechat" 输出:package:com.tencent.mm步骤 2:获取 APK 安装路径adb shell pm path <包名> 示例:adb shell pm path com.tencent.mm输出:package:/data/app/com.tencent.mm-XXXXXXXX/base.apk步骤 3:拉取 APK 到本地adb pull <设备上的APK路径> <本地保存路径> 示例:adb pull /data/app/com.tencent.mm-XXXXXXXX/base.apk ./wechat.apk方法 2:直接通过 adb pull 导出(适用于系统应用)系统应用通常安装在 /system/app/ 或 /system/priv-app/ 目录下,可直接拉取:adb pull /system/app/<应用名>/<应用名>.apk ./local.apk示例(导出系统计算器):adb pull /system/app/Calculator/Calculator.apk ./calculator.apk方法 3:使用 adb shell + cat 导出(适用于无权限路径)如果 adb pull 报错(如 Permission denied),可通过 cat 重定向到本地:adb shell cat <设备上的APK路径> > ./local.apk示例:adb shell cat /data/app/com.tencent.mm-XXXXXXXX/base.apk > ./wechat.apk注意:此方法在 Windows 上需使用 adb exec-out 替代 adb shell cat:adb exec-out cat /data/app/com.tencent.mm-XXXXXXXX/base.apk > wechat.apk方法 4:批量导出所有应用(需 root)如果设备已 root,可以批量导出所有 APK:adb shell su cd /data/app for apk in $(find . -name "*.apk"); do adb pull /data/app/$apk ./${apk//\//_} done 常见问题Permission denied 错误原因:/data/app/ 下的 APK 需要 root 权限访问。解决方案:使用 adb exec-out(无需 root):adb exec-out cat /data/app/com.example.app/base.apk > app.apk或通过 adb root 重启 adb 为 root 模式(需设备支持)。导出的 APK 无法安装可能是拆分 APK(Split APK),需导出所有分包(如 base.apk + config.xxhdpi.apk)。使用 adb shell pm list packages -f 查看完整路径。查找系统应用路径系统应用通常位于:/system/app/(普通系统应用)/system/priv-app/(特权应用)/vendor/app/(厂商定制应用)完整示例导出微信 APK# 1. 查找包名 adb shell pm list packages | grep "wechat" # 输出: package:com.tencent.mm # 2. 获取 APK 路径 adb shell pm path com.tencent.mm # 输出: package:/data/app/com.tencent.mm-XXXXXXXX/base.apk # 3. 拉取 APK(方法1) adb pull /data/app/com.tencent.mm-XXXXXXXX/base.apk ./wechat.apk # 若失败,改用方法3(无需 root) adb exec-out cat /data/app/com.tencent.mm-XXXXXXXX/base.apk > wechat.apk通过以上方法,你可以轻松导出设备上的 APK 文件。如果需要导出 数据+APK(如备份应用),可以结合 adb backup 命令(需应用允许备份)。
  • [技术干货] adb模拟键盘/鼠标
    在 Android 调试中,adb 可以通过 input 命令模拟键盘输入、鼠标点击、触摸操作等,适用于自动化测试或远程控制设备。以下是详细用法和示例:一、模拟键盘输入1. 发送文本adb shell input text "HelloWorld" 注意:文本需用英文双引号包裹。空格会被忽略,需用 %s 代替(如 Hello%sWorld 输出 Hello World)。特殊字符(如 @、#)可能需转义或使用 Unicode 编码。2. 发送按键事件adb shell input keyevent <KEYCODE> 常用键值:键名KEYCODE说明HOME3返回主屏幕BACK4返回键POWER26电源键MENU82菜单键VOLUME_UP24音量加VOLUME_DOWN25音量减ENTER66回车键DEL67退格键SPACE62空格键TAB61Tab 键ESC111Escape 键F1~F12131~142功能键DPAD_UP19方向键上DPAD_DOWN20方向键下DPAD_LEFT21方向键左DPAD_RIGHT22方向键右示例:adb shell input keyevent 3 # 返回主屏幕 adb shell input keyevent 4 # 返回上一级 adb shell input keyevent 66 # 模拟回车键 二、模拟鼠标/触摸操作1. 单击屏幕adb shell input tap <x> <y> 示例:adb shell input tap 500 1000 # 在屏幕坐标 (500, 1000) 处点击 2. 长按屏幕adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms> 说明:通过模拟滑动实现长按(起点和终点相同,持续时间较长)。示例:adb shell input swipe 500 1000 500 1000 2000 # 长按 (500, 1000) 2秒 3. 滑动屏幕adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms> 示例:adb shell input swipe 300 1000 300 500 300 # 从 (300, 1000) 向上滑动到 (300, 500),耗时 300ms 4. 滚动(适用于 ListView/RecyclerView)adb shell input swipe <x> <y1> <x> <y2> <duration_ms> 示例:adb shell input swipe 500 1500 500 500 500 # 垂直滚动(从上到下) 三、模拟手势操作1. 捏合缩放(Zoom In/Out)# 缩小(捏合) adb shell input swipe 400 800 600 600 200 adb shell input swipe 600 600 400 800 200 # 放大(展开) adb shell input swipe 400 800 200 1000 200 adb shell input swipe 200 1000 400 800 200 2. 拖拽adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms> 示例:adb shell input swipe 500 1000 800 1000 500 # 将 (500, 1000) 拖拽到 (800, 1000) 四、高级用法1. 获取屏幕分辨率adb shell wm size输出示例:Physical size: 1080x1920用途:根据分辨率计算坐标(如居中点击):adb shell input tap 540 960 # 1080/2=540, 1920/2=960 2. 获取当前窗口信息adb shell dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp' 用途:确认当前活动窗口,避免操作到错误界面。3. 组合操作(脚本化)# 示例:解锁屏幕 + 进入应用 adb shell input keyevent 26 # 唤醒屏幕 adb shell input swipe 500 1500 500 800 300 # 滑动解锁 adb shell input tap 200 300 # 点击应用图标 五、注意事项坐标系统:Android 屏幕坐标原点 (0, 0) 在左上角,向右为 X 轴正方向,向下为 Y 轴正方向。部分设备可能因导航栏/状态栏导致实际可用区域偏移。权限问题:模拟输入需要 adb 有足够权限(通常需 root 才能操作系统级输入)。兼容性:不同 Android 版本可能对 input 命令的支持有差异(如 Android 10+ 限制后台模拟输入)。调试工具:使用 adb shell getevent 或 adb shell sendevent 可实现更底层的输入模拟(需 root)。六、完整示例1. 自动输入文本 + 点击按钮adb shell input text "Hello%sWorld" # 输入文本(空格用 %s 代替) adb shell input tap 500 1200 # 点击坐标 (500, 1200) 2. 模拟滑动解锁adb shell input swipe 500 1800 500 800 500 # 从下往上滑动解锁 通过 adb input 命令,你可以轻松实现自动化测试、远程控制或模拟用户操作。结合 adb shell 的其他命令(如 screencap 截图),可以构建完整的自动化脚本。
  • [技术干货] adb 安装/卸载应用
    在 Android 调试中,使用 adb(Android Debug Bridge)安装或卸载应用是常见操作。以下是详细步骤和注意事项:一、安装应用(APK 文件)基本命令adb install [选项] <apk文件路径> 常用选项普通安装adb install app.apk若应用已存在,会报错:Failure [INSTALL_FAILED_ALREADY_EXISTS]。覆盖安装(保留数据)adb install -r app.apk-r 表示替换现有应用,保留用户数据和缓存。快速安装(跳过缓存优化)adb install -t app.apk-t 允许测试包(Test Packages)安装,常用于调试。安装到特定设备(多设备时)adb -s <设备序列号> install app.apk通过 adb devices 查看设备序列号。安装到 SD 卡(需设备支持)adb install -s app.apk-s 表示安装到 SD 卡(部分设备可能忽略此选项)。常见错误INSTALL_FAILED_UPDATE_INCOMPATIBLE应用签名不匹配,需先卸载旧版本:adb uninstall <包名> adb install app.apkINSTALL_FAILED_VERSION_DOWNGRADE尝试降级安装,需添加 -d 允许降级:adb install -r -d app.apk二、卸载应用基本命令adb uninstall <包名> 包名可通过以下方式获取:查看 APK 的 AndroidManifest.xml 中的 package 属性。通过 adb shell pm list packages 列出所有包名。示例adb uninstall com.example.app保留数据卸载adb uninstall -k <包名> -k 表示保留应用数据和缓存目录(/data/data/<包名>)。三、其他实用命令查看已安装应用列表adb shell pm list packages过滤特定应用:adb shell pm list packages | grep "example" 清除应用数据adb shell pm clear <包名> 查看 APK 安装路径adb shell pm path <包名> 从设备拉取 APKadb pull $(adb shell pm path <包名> | cut -d: -f2) ./app.apk四、注意事项设备连接确保设备已启用 USB 调试(开发者选项中开启)。通过 adb devices 确认设备已连接。权限问题部分操作需要 root 权限(如访问系统应用目录)。签名验证覆盖安装时,新 APK 必须与旧版本签名一致(除非使用 -d 降级)。系统应用卸载系统应用需谨慎,可能导致设备不稳定:adb shell pm uninstall --user 0 <包名> 五、完整流程示例安装应用adb install -r -t app-debug.apk卸载应用adb uninstall com.example.app验证安装状态adb shell pm list packages | grep "com.example.app" 通过以上命令,你可以高效地管理 Android 设备上的应用。遇到问题时,可通过 adb logcat 查看详细日志。
  • [技术干货] adb 关机
    通过 ADB(Android Debug Bridge) 可以远程控制手机关机,无需手动操作物理按键。以下是详细方法及注意事项:一、基础关机命令1. 标准关机(推荐)adb shell am broadcast -a android.intent.action.ACTION_REQUEST_SHUTDOWN效果:触发系统关机流程(等同于长按电源键选择“关机”)。特点:兼容性最好,适用于大多数 Android 设备(包括小米、华为、三星等)。部分手机会弹出关机确认对话框(需手动确认)。2. 强制关机(无确认)adb shell reboot -p或adb shell poweroff效果:立即关闭手机电源(无确认提示)。注意:reboot -p 是 Linux 标准关机参数,部分厂商可能禁用;poweroff 是 Android 底层命令,但部分系统可能限制访问权限。二、分机型适配方案1. 华为/荣耀问题:部分机型会拦截 ADB 关机指令。解决方案:adb shell am start -n com.android.settings/.PowerUsageSummary # 尝试打开电源设置(间接关机) adb shell input keyevent 26 # 模拟按下电源键(可能触发锁屏而非关机) 终极方案:长按电源键 10-15秒 强制断电。2. 小米/Redmi特殊要求:需在 开发者选项 中开启 “USB 调试(安全设置)”。推荐命令:adb shell am broadcast -a miui.intent.action.POWER_OFF # 小米定制关机指令(部分机型适用) 3. 三星问题:系统可能阻止后台进程关机。解决方案:adb shell settings put global device_provisioned 1 # 绕过部分限制(不保证有效) adb shell reboot -p4. OPPO/vivo关键步骤:确保已授权 “USB 调试(安装应用)” 权限;使用标准命令:adb shell am broadcast -a android.intent.action.ACTION_REQUEST_SHUTDOWN三、高级技巧1. 通过 input 命令模拟按键(需屏幕解锁)adb shell input keyevent KEYCODE_POWER # 模拟按下电源键 adb shell input swipe 500 1500 500 500 200 # 模拟滑动关机(部分机型无效) 限制:需手机屏幕处于解锁状态,且部分厂商禁用虚拟按键模拟。2. 定时关机脚本Linux/Mac 脚本:#!/bin/bash # 设置关机延迟(单位:秒) DELAY=30 echo "手机将在 $DELAY 秒后关机..." sleep $DELAY adb shell am broadcast -a android.intent.action.ACTION_REQUEST_SHUTDOWN echo "关机指令已发送。" Windows 批处理脚本:@echo off set /p DELAY="请输入关机延迟(秒): " echo 手机将在 %DELAY% 秒后关机... timeout /t %DELAY% /nobreak >nul adb shell am broadcast -a android.intent.action.ACTION_REQUEST_SHUTDOWN echo 关机指令已发送。 pause3. 无线 ADB 关机(Android 11+)手机开启 无线调试(设置 → 开发者选项 → 无线调试);记录手机 IP 和端口(如 192.168.1.100:5555);电脑执行:adb connect 192.168.1.100:5555 adb shell reboot -p四、常见问题解决1. 命令报错 Permission denied原因:系统限制后台进程关机权限。解决:尝试使用 su -c reboot -p(需 root 权限);更换为 adb shell am broadcast 广播方式。2. 手机无响应强制断电:长按电源键 10-15秒 直到屏幕熄灭;拆电池(可拆卸电池机型):直接拔电池重启。3. 关机后自动重启可能原因:系统崩溃(尝试进入 Recovery 模式清除缓存);硬件故障(如电源键短路)。解决:adb reboot recovery # 进入 Recovery 模式 # 在 Recovery 中选择 "Wipe cache partition" → "Reboot system now" 五、总结对比命令效果兼容性需确认adb shell am broadcast -a android.intent.action.ACTION_REQUEST_SHUTDOWN标准关机(推荐)⭐⭐⭐⭐⭐✅(部分机型)adb shell reboot -p强制关机⭐⭐⭐⭐❌adb shell poweroff底层关机⭐⭐⭐❌模拟电源键间接关机⭐⭐❌推荐操作流程:优先尝试标准广播命令;若无效,使用 reboot -p 或 poweroff;极端情况下长按电源键强制断电;遇到系统异常时进入 Recovery 模式修复。
  • adb 重启手机
    通过 ADB(Android Debug Bridge) 可以轻松重启手机,无需物理按键操作。以下是详细方法及注意事项:一、基础重启命令1. 标准重启(推荐)adb reboot 效果:手机立即重启(等同于长按电源键选择“重启”)。适用场景:大多数手机通用,包括小米、华为、三星、OPPO等。2. 重启到 Recovery 模式adb reboot recovery效果:手机重启后进入 Recovery 界面(用于刷机、清除数据等)。注意:部分厂商定制 Recovery 可能需要额外命令(如小米需先解锁 Bootloader)。3. 重启到 Fastboot 模式(Bootloader)adb reboot bootloader效果:手机重启后进入 Fastboot 界面(用于刷机、解锁等)。适用场景:需要连接电脑进行线刷操作时使用。二、特殊情况处理1. 手机卡死无响应若手机完全卡死(无法操作屏幕),ADB 可能仍能强制重启:adb reboot 原理:ADB 直接向系统发送重启指令,绕过用户界面。失败时:尝试以下替代方案:强制断电:长按电源键 10-15秒 强制关机,再开机;拆电池(可拆卸电池机型):直接拔电池重启。2. 无 Root 权限的重启限制普通情况:ADB 重启无需 root 权限,所有手机均支持;特殊场景:若手机系统被修改(如定制 ROM),部分命令可能失效,需尝试:adb shell am start -n com.android.commands.monkey/.MonkeyCommandReboot # 某些定制系统适用 3. 通过 power 命令模拟按键(需 root)若 ADB 基础命令无效且已 root,可模拟长按电源键:adb shell input keyevent 26 # 模拟按下电源键(可能触发锁屏而非重启) adb shell su -c "reboot" # 需 root 权限的强制重启 三、分机型注意事项品牌特殊说明华为/荣耀部分机型需开启 “允许通过 HDB 连接设备”(设置 → 安全 → 更多安全设置)。小米/Redmi需在 开发者选项 中开启 “USB 调试(安全设置)” 才能使用 ADB 重启。三星部分旧机型需先解锁 OEM 解锁 才能进入 Fastboot/Recovery 模式。OPPO/vivo需授权 “USB 调试(安装应用)” 权限,否则 ADB 命令可能被拦截。四、自动化脚本示例若需将重启命令集成到脚本中(如定时重启),可使用以下 Bash 脚本(Linux/Mac):#!/bin/bash # 检查设备是否连接 if ! adb devices | grep -q "device$"; then echo "错误:未检测到连接的设备!" exit 1 fi # 执行重启 echo "正在重启手机..." adb reboot echo "重启命令已发送。" Windows 批处理脚本:@echo off adb devices | find "device" > nul if %errorlevel% neq 0 ( echo 错误:未检测到连接的设备! pause exit /b ) echo 正在重启手机... adb reboot echo 重启命令已发送。 pause五、常见问题解决1. 命令报错 device not found原因:USB 调试未开启、驱动未安装、数据线故障。解决:检查手机是否弹出 “允许 USB 调试?” 提示并点击确认;更新 USB 驱动(Windows 需特别注意);更换数据线或 USB 接口(优先使用后置接口)。2. 重启后卡在开机画面原因:系统崩溃、缓存冲突。解决:进入 Recovery 模式,选择 “清除缓存”;若无效,选择 “恢复出厂设置”(会删除数据,谨慎操作)。3. 无线 ADB 重启若通过 Wi-Fi 连接设备(Android 11+):adb connect 192.168.x.x:5555 # 替换为手机IP和端口 adb reboot 总结命令效果权限要求adb reboot标准重启无adb reboot recovery重启到 Recovery 模式无adb reboot bootloader重启到 Fastboot 模式无adb shell su -c reboot强制重启(需 root)root推荐操作流程:连接手机并开启 USB 调试;执行 adb reboot 测试基础功能;如需进入特殊模式,使用 recovery 或 bootloader 参数;遇到问题时检查设备连接或尝试强制断电重启。
  • [技术干货] adb 获取电池电量
    ADB 可以通过特定命令获取电池电量信息,但需要手机已开启 USB 调试模式(部分机型还需授权 ADB 访问权限)。以下是详细方法和注意事项:一、通过 dumpsys battery 命令获取电量连接手机并开启 ADB用数据线连接手机和电脑,确保手机已开启 开发者选项 和 USB 调试。在电脑命令行(Windows:CMD/PowerShell;Mac/Linux:Terminal)输入以下命令,确认设备已识别:adb devices若显示设备序列号(如 XXXXXX device),说明连接成功。执行电池信息查询命令输入以下命令获取电池详细信息:adb shell dumpsys battery关键输出字段(以实际输出为例):Current Battery Service state: AC powered: false # 是否连接充电器 USB powered: true # 是否通过USB供电 Wireless powered: false # 是否无线充电 Status: 2 # 充电状态(2=充电中,5=放电中) Health: 1 # 电池健康度(1=良好) Present: true # 电池是否安装 Level: 85 # 当前电量百分比(重点!) Scale: 100 # 电量最大值(通常为100) Voltage: 4320 # 电池电压(mV) Temperature: 280 # 电池温度(0.1℃单位,此处为28℃) Technology: Li-ion # 电池类型电量百分比计算:直接读取 Level 字段值(如 85 即表示 85%)。二、简化命令(仅获取电量百分比)若只需快速查看电量,可使用以下命令过滤输出:adb shell dumpsys battery | grep -E "level|status" # Linux/Mac adb shell dumpsys battery | findstr "level status" # Windows 或直接提取 Level 值(适用于脚本自动化):adb shell dumpsys battery | awk '/level:/ {print $2}' # Linux/Mac adb shell dumpsys battery | find "level:" & set /p var=& echo %var:~7% # Windows(复杂但可行) 三、通过 batterystats 获取更详细数据(需 root)若手机已 root,可获取更深入的电池统计信息(如耗电排名):adb shell dumpsys batterystats --reset # 重置统计数据(可选) adb shell dumpsys batterystats --charged # 查看充电后的统计 注意:普通用户无需使用此命令,且部分数据需要 root 权限。四、常见问题解决命令无输出或报错原因:USB 调试未开启、ADB 版本过旧、手机未授权。解决:检查手机是否弹出 “允许 USB 调试?” 提示并点击确认;更新 ADB 工具到最新版(下载地址);重启手机或更换数据线/USB 接口。电量显示不准确原因:电池老化、系统校准错误。解决:尝试重启手机;若电量长期跳变(如从 50% 突然到 20%),可能是电池损坏,需更换。无线调试(无需数据线)Android 11+ 支持 无线 ADB 调试:手机开启 开发者选项 → 无线调试;点击 使用配对码配对设备,记录 IP 地址和端口;电脑输入:adb connect 手机IP:端口 # 例如 adb connect 192.168.1.100:5555 adb shell dumpsys battery五、替代方案(无需 ADB)如果无法使用 ADB,可通过以下方式查看电量:手机状态栏:直接下拉通知栏查看(最简单);系统设置:进入 设置 → 电池 查看详细信息;第三方 App:如 AccuBattery、GSam Battery Monitor(需安装)。总结方法命令/操作适用场景ADB 获取电量adb shell dumpsys battery开发者调试、自动化脚本快速过滤电量`adb shell dumpsys batterygrep level`无线调试(Android 11+)adb connect IP:端口无需数据线的远程调试普通用户替代方案下拉状态栏/设置→电池无技术背景时推荐:普通用户优先使用手机自带功能;开发者或需要自动化场景时,ADB 是更灵活的选择。
  • [其他问题] Android开发camera2配置EIS预览流卡死
    华为mate20x录制1920x1080或者4k 60fps视频并且设置EIS,配流后预览卡死,而30fps没问题,请问是否是这两个分辨率不支持EIS?以下为部分配置:打开cameraId[0]后进行配置CaptureRequestSurfaceTexture surfaceTexture = mTextureView1.getSurfaceTexture();surfaceTexture.setDefaultBufferSize(1920, 1080);mPreviewSurface = new Surface(surfaceTexture);mVideoRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);mVideoRequestBuilder.addTarget(mPreviewSurface);mVideoRequestBuilder.addTarget(mRecorderSurface);Range<Integer> fpsRange = fpsRanges[fpsRanges.length - 1];Range aeFps = new Range(60, 60);System.out.println("carbry select max fps: " + aeFps);mVideoRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, aeFps);// 设置EISmVideoRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON);mVideoRequestBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_OFF);// 配流List<Surface> surfaceList = Arrays.asList(mPreviewSurface, mRecorderSurface); if (!isHighFps) { try { mCameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { System.out.println("carbry session config success!"); mCameraSession = cameraCaptureSession;// startPreview(); startRepeatingRequest(); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { System.out.println("carbry session config failed!!!"); } }, mCameraHandler); } catch (CameraAccessException e) { throw new RuntimeException(e); } } else { try { mCameraDevice.createConstrainedHighSpeedCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { System.out.println("carbry highFps session config success!"); mCameraSession = cameraCaptureSession; startRepeatingRequest(); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { System.out.println("carbry highFps session config failed!!!"); } }, mCameraHandler); } catch (CameraAccessException e) { throw new RuntimeException(e); } } }// 开始录像预览private void startRepeatingRequest() { try { mCameraSession.setRepeatingRequest(mVideoRequestBuilder.build(), null, mCameraHandler); } catch (CameraAccessException e) { throw new RuntimeException(e); }}
  • [技术干货] 判断Android APP使用了Tauri 这个跨端框架的操作方法【转】
    一、Tauri 的核心特征• 技术栈:• 前端:Web 技术(HTML/CSS/JS)渲染界面。• 后端:Rust 语言编译的原生逻辑(通过 WebView 交互)。• 移动端实现(实验性):• 嵌入 WebView(如 Android 的 WebView 或 Chrome Custom Tabs)。• 包含 Rust 编译的 .so 库文件。• 使用 tauri-mobile 或实验性工具链构建。二、安装包结构分析1. 解压 APK将 .apk 文件重命名为 .zip 并解压,检查以下特征文件:• lib/ 目录:若应用包含 Tauri 的 Rust 后端,可能包含以下库文件(需根据架构区分):• libtauri.so(Tauri 核心库,假设存在)• libtauri_mobile.so(实验性移动端支持库)• assets/ 目录:Tauri 的 Web 前端资源可能存放在以下路径:• assets/www/:HTML/CSS/JS 文件。• assets/tauri.conf.json:Tauri 配置文件(若沿用桌面端配置格式)。三、代码特征验证1. 反编译 Java 代码使用工具(如 JADX)检查以下标识:• WebView 初始化逻辑:Tauri 可能通过 WebView 加载本地资源:123456789public class MainActivity extends AppCompatActivity {    private WebView webView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        webView = new WebView(this);        webView.loadUrl("file:///android_asset/www/index.html");    }}• Rust 库调用:若应用通过 JNI 调用 Rust 代码,可能包含以下代码:1234public class TauriBridge {    static { System.loadLibrary("tauri"); }    public static native void invokeRustMethod(String command);}2. 检查 Web 资源查看 assets/www/ 下的文件:• Tauri 前端 API 调用:123// 调用 Rust 后端(假设 Tauri 移动端 API 与桌面端类似)import { invoke } from '@tauri-apps/api';invoke('show_message', { title: 'Hello', message: 'Tauri Mobile!' });• 配置文件:查找 tauri.conf.json 或类似配置:123456789{  "build": {    "distDir": "../www",    "devPath": "http://localhost:3000"  },  "tauri": {    "embeddedServer": { "active": true }  }}四、依赖库与资源1. Rust 编译产物通过 nm 或 readelf 工具检查 .so 文件的符号表,查找 Tauri 相关函数:12345# 示例(需将 libtauri.so 替换为实际文件名)nm -D libtauri.so | grep -i "tauri_"# 输出可能包含:# tauri_init# tauri_invoke2. 依赖库标识若应用使用 tauri-mobile 工具链,可能包含以下依赖:• cargo 构建的 Rust 库。• android_logger(Rust 日志库)。五、运行时检测1. Logcat 日志过滤运行应用时,通过 adb logcat 过滤 Rust 或 Tauri 相关日志:1234adb logcat | grep -iE "tauri|rust"# 示例输出(假设存在):# I/rust: Tauri initialized# D/tauri_mobile: Invoking Rust method: show_message2. WebView 调试若应用允许调试,通过 Chrome 的 chrome://inspect 连接 WebView 控制台:• 检查全局对象:1console.log(window.__TAURI__); // 输出 Tauri 对象(若存在)六、与其他框架的区分特征Tauri(实验性)CapacitorCordova后端语言RustJava/Kotlin(原生插件)Java/Objective-C(原生插件)Web 资源目录assets/www/ + Rust 库assets/public/assets/www/配置文件tauri.conf.jsoncapacitor.config.jsonconfig.xml核心库文件libtauri.so(假设存在)libcapacitor-android.solibcordova.so
  • [技术干货] Android 适配器 (Adapter) 讲解教程
    1. 什么是适配器?在 Android 开发中,适配器 (Adapter) 是连接数据源和 UI 组件的桥梁。它负责将数据转换成 UI 组件能够显示的形式,例如将字符串列表转换成 ListView 中的每一项。2. 适配器的作用数据绑定: 将数据源中的数据绑定到 UI 组件上。视图复用: 复用已创建的视图,提高列表滚动的流畅度。数据处理: 对数据进行过滤、排序等操作。3. 常用的适配器类型Android 提供了多种适配器类型,常用的有以下几种:ArrayAdapter: 适用于简单的文本列表,数据源可以是数组或 List。SimpleAdapter: 适用于复杂的列表项,数据源可以是 List<Map<String, ?>>,每个 Map 对应列表项中的数据。BaseAdapter: 自定义适配器的基类,可以实现更复杂的列表项布局和逻辑。RecyclerView.Adapter: 用于 RecyclerView 的适配器,提供更强大的视图复用机制。4. 适配器的使用步骤以 ArrayAdapter 为例,介绍适配器的使用步骤:4.1 创建数据源java复制List<String> data = new ArrayList<>();data.add("Apple");data.add("Banana");data.add("Orange");4.2 创建适配器java复制ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data);第一个参数:Context 对象第二个参数:列表项的布局资源 ID第三个参数:数据源4.3 将适配器设置给 UI 组件java复制ListView listView = findViewById(R.id.list_view);listView.setAdapter(adapter);5. 自定义适配器如果需要实现更复杂的列表项布局和逻辑,可以继承 BaseAdapter 或 RecyclerView.Adapter 来自定义适配器。5.1 继承 BaseAdapter重写 getCount(), getItem(), getItemId(), getView() 方法。在 getView() 方法中实现列表项的布局和数据绑定。5.2 继承 RecyclerView.Adapter创建 ViewHolder 类,用于缓存视图。重写 onCreateViewHolder(), onBindViewHolder(), getItemCount() 方法。在 onBindViewHolder() 方法中实现列表项的数据绑定。6. 总结适配器是 Android 开发中非常重要的组件,掌握适配器的使用可以大大提高开发效率。7. 进阶学习学习如何使用 RecyclerView 实现更复杂的列表布局。学习如何使用 DiffUtil 优化 RecyclerView 的更新效率。学习如何使用 Data Binding 简化数据绑定代码。8. 参考文档Android Developers: AdapterAndroid Developers: RecyclerViewAndroid Developers: Data Binding
  • [技术干货] android 中的 windowSoftInputMode
    android:windowSoftInputMode属性在Android应用中用于控制软键盘的显示和行为方式。该属性可以在AndroidManifest.xml文件中的对应Activity标签内进行配置。以下是android:windowSoftInputMode属性的各个参数及其含义:状态参数(state…)stateUnspecified:未指定状态。当没有设置android:windowSoftInputMode属性时,软件默认采用此交互方式。系统会根据界面采取相应的软键盘显示模式。stateUnchanged:状态不改变。当前界面的软键盘状态取决于上一个界面的软键盘状态。stateHidden:软键盘隐藏。设置此属性后,不管上个界面是什么状态,也不管当前界面有没有输入的需求,软键盘都不会显示。stateAlwaysHidden:软键盘始终隐藏。无论软键盘是否弹出,都隐藏软键盘。这通常用于全屏应用,或者当用户希望点击屏幕来手动显示软键盘时。stateVisible:软键盘显示。设置此属性后,会强制召唤出软键盘,即使在界面上没有输入框的情况下也会如此。stateAlwaysVisible:软键盘始终显示。无论软键盘是否弹出,都显示软键盘。这通常不常用,因为它可能会干扰用户与应用的正常交互。调整参数(adjust…)adjustUnspecified:未指定调整方式。这是软键盘与页面之间显示关系的未指定状态、默认设置状态。这种状态下,系统会根据界面选择不同的模式。如果界面里有可滚动的控件(如RecyclerView、ScrollView等),系统会缩小可以滚动的界面的大小,以保证即使软键盘弹出了,也能够看到所有的控件。如果界面里没有可滚动的控件,那么软键盘可能会盖住一些控件(布局的位置会发生变化,会让获取了焦点的控件显示出来,视情况隐藏可能会隐藏一些控件)。adjustResize:调整窗口大小。表示Activity的主窗口总是会被调整大小,以保证软键盘的显示空间。如果界面中有可滑动控件,显示效果与adjustUnspecified一样。如果界面中没有可滑动控件,软键盘可能会盖住一些控件(布局的位置不会发生变化,可能获取了焦点的控件被软键盘盖住)。adjustPan:调整窗口位置。当软键盘弹出时,系统会通过布局的移动,以保证用户要进行输入的输入框在用户的视线范围内。如果界面没有可滑动控件,显示效果和adjustUnspecified效果一样。如果界面有可滑动控件,在软键盘显示的时候,可能会有一些内容显示不出来。组合参数除了单独使用上述参数外,还可以使用“state…|adjust…”的形式进行组合设置,例如:adjustPan|stateAlwaysHidden:先调整窗口位置以使活动的内容不被软键盘遮挡,然后隐藏软键盘。adjustResize|stateAlwaysHidden:先调整窗口大小以适应软键盘,然后隐藏软键盘。在实际开发中,可以根据具体需求选择合适的参数或参数组合来设置android:windowSoftInputMode属性,以确保软键盘的显示和行为方式符合应用的设计要求。同时,也需要注意不同的设备和输入法可能会有不同的行为表现,因此可能需要进行相应的调整和测试。
  • [其他问题] Android NFC开发 华为NFC无法唤起备忘录以外的应用
     试开发一个能被NFC唤起的应用,手上有nfc标签,按照开发者官网的文档,设置了NDEF_DISCOVERED等action和mimeType “text/plain”  1.在其他厂商手机安装应用贴近nfc能正常被唤起 2.而手上安卓10的P40 pro 和鸿蒙系统的p40都只能唤起备忘录,且标签内容被直接写入备忘录,贴一次写一次 3.尝试卸载备忘录,贴nfc标签除了震动无其他反应  如何才能在华为手机上,正常使用文本的nfc标签唤起应用功能呢? 
  • [经验交流] 安卓端微信读书导入pdf文件不能下载图片
    换格式能解决吗有什么解决措施吗,想要彻底数字化学习导入教材就是不显示图片
  • [问题求助] Android老版本的obs SDK从Maven下不到了,比如3.19.12
    如题,Android老版本的obs SDK从Maven下不到了,比如3.19.12
  • [问题求助] Android 初始化后登录报错,错误码33554448
    TsdkLoginFailedInfo{reasonCode=33554448, reasonDescription='[TSDK_E_LOGIN_ERR_SERVICE_ERROR]:server exception.', lockInterval=0, residualRetryTimes=0}这个错误码是什么意思,有没有什么地方可以找到错误对应的说明