一步步教你如何简单自定义 ADB 指令

adb 指令相信大家都用得不少,但是自定义 adb 指令不知道大家又试过没有?最近公司有一个需求,需要自定义 adb 指令来对手机硬件进行测试,这篇博客我们就来一起聊一聊我的实现方法,希望能帮助到有相似需求的朋友。

我们先来看看常用的 adb 指令,比如启动活动和服务等:

启动活动: adb shell am start
启动服务: adb shell am startservice 

具体指令和参数可参考官方文档:Android Debug Bridge (adb)

上面的指令中,我们通过 am 指令调用 AMS 的功能。该是放置在 system/bin 目录上的,我们可以将其打开:

#!/system/bin/sh

if [ "$1" != "instrument" ] ; then
    cmd activity "$@"
else
    base=/system
    export CLASSPATH=$base/framework/am.jar
    exec app_process $base/bin com.android.commands.am.Am "$@"
fi

可以发现,这其实就是一个 shell 文件,在该文件中通过 shell 语言调用 cmd activity 指令的功能,从而调用 AMS 的功能。在这里我们不过多去分析 am 指令的实现原理,因为重点并不在此。至于 shell 语言,我建议大家可以去 Shell 教程 稍微学一下,它其实并不难,特别是对于有编程基础的大家来说。
那么 am 文件是如何编译进手机目录的呢?源码的位置位于 frameworks\base\cmds\am,在这里面有一个 am 文件,和上面的代码是一模一样的。除此之外我们还能发现一些 src 文件夹和其他一些文件,其中我们需要重点关注的是 Android.mk 这个文件。

# Copyright 2008 The Android Open Source Project
#
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
    $(call all-java-files-under, src) \
    $(call all-proto-files-under, proto)
LOCAL_MODULE := am
LOCAL_PROTOC_OPTIMIZE_TYPE := stream
include $(BUILD_JAVA_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := am
LOCAL_SRC_FILES := am
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_MODULE_TAGS := optional
include $(BUILD_PREBUILT)

上面的 mk 文件主要是做了两件事,第一件事就是通过 include $(BUILD_JAVA_LIBRARY) 语句将 src 和 proto 文件夹下的文件编译成了 am.jar。第二件事情就是通过 include $(BUILD_PREBUILT) 将 am 文件拷贝到手机目录中。并且通过 LOCAL_MODULE 属性将模块名定义为 am。

在编译 Android 源码的时候,系统会从 PRODUCT_PACKAGES 属性列表中读取需要编译合入的模块,在 build/make/target/product/base.mk 文件下,有以下语句将 am 加进了 PRODUCT_PACKAGES 属性当中,因此在源码编译时,会将 am 指令编译进去。当然,在源码环境下通过 mmm frameworks/base/cmds/am/ 指令也能对模块进行编译。


PRODUCT_PACKAGES += \
    20-dns.conf \
    95-configured \
    org.apache.http.legacy.boot \
    appwidget \
    appops \
    am \
    android.policy \
    android.test.base \
    android.test.mock \
    android.test.runner \
    app_process \
    applypatch \
    audioserver \
    bit \
    blkid \

有了上面的知识,要自定义 adb 指令就显得容易很多了。假设我们有一个需求,需要自定义一个 adb 指令模拟按键输入,那么我们就可以写一个名为 testinput 的 shell 文件:

#!/system/bin/sh

input keyevent  $1

接着写对应的 Android.mk

# Copyright 2008 The Android Open Source Project
#
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := testinput
LOCAL_SRC_FILES := testinput
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_MODULE_TAGS := optional
include $(BUILD_PREBUILT)

并且将 testinput 加入 build/make/target/product/base.mk 的 PRODUCT_PACKAGES 列表当中,当编译系统源码时,就会将 testinput 编译进 system/bin 中,我们就可以通过 adb 指令去使用它了。没有源码的朋友也可以用有 root 权限的机器或者模拟器来试验,通过以下指令将 testinput 文件 push 到 system/bin 目录进行使用。

adb root
adb remount
adb push <path>/testinput /system/bin

当然,上面的例子其实并没有什么实际的用处,仅仅是拐了个弯调用了 input 指令而已,为的是方便大家理解。

在实际应用中,自定义 adb 指令能够使得测试更加方便快捷,提高测试效率。我们可以通过自定义 adb 非常方便地调用上层或底层实现的功能。

比如在我司要求的音频测试中,我的实现思路是根据输入的指令启动指定的服务,并且在服务中根据传入的参数设置音频的输入输出端口和模式等。这样做的好处在于能大大缩小 adb 指令的长度和复杂度,并且对于不懂代码的测试人员比较友好,容易理解。

在使用 adb 的时候我们还有一些细节需要注意。我们都知道在 Android 8.0 及以上,启动一个没有正在运行的进程的服务需要使用 Context 的 startForegroundService 方法,并且服务启动后需要调用该服务的 startForeground 方法,否则会导致报错,这个限制在 adb 启动服务的情景中也是一样的。通过查看
frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
的以下代码我们很清楚的显示了这一点:

    @Override
    public int onCommand(String cmd) {
        if (cmd == null) {
            return handleDefaultCommands(cmd);
        }
        final PrintWriter pw = getOutPrintWriter();
        try {
            switch (cmd) {
                case "start":
                case "start-activity":
                    return runStartActivity(pw);
                case "startservice":
                case "start-service":
                    return runStartService(pw, false);
                case "startforegroundservice":
                case "startfgservice":
                case "start-foreground-service":
                case "start-fg-service":
                    return runStartService(pw, true);
                case "stopservice":
                case "stop-service":
                    return runStopService(pw); 
                .....
            }
        }
		......
	}

    ......

    int runStartService(PrintWriter pw, boolean asForeground) throws RemoteException {
        final PrintWriter err = getErrPrintWriter();
        Intent intent;
        try {
            intent = makeIntent(UserHandle.USER_CURRENT);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        if (mUserId == UserHandle.USER_ALL) {
            err.println("Error: Can't start activity with user 'all'");
            return -1;
        }
        pw.println("Starting service: " + intent);
        pw.flush();
        ComponentName cn = mInterface.startService(null, intent, intent.getType(),
                asForeground, SHELL_PACKAGE_NAME, mUserId);
        ......
    }

查看代码的 14 行和第 19 行我们可以发现,通过 adb 启动服务的时候都是通过 runStartService 方法启动的,而这个方法根据第二个布尔值参数来决定是否是启动前台服务,而第 45 行的 mInterface.startService 方法其实最终会调用到 AMS 的 startService 方法。

因此在启动一个没有运行中进程的服务时(比如在我司的音频测试需求中启动服务),我们需要通过 adb shell startforegroundservice 或者 adb shell startfgservice 才能正常运行。

再或者,在测试过程中,测试人员需要设置标志位对测试过的机器进行标记,此时我们一般会通过调用 adb shell setpropadb shell getprop 方法设置属性。但是在设置的时候我们需要注意属性的开头需要是 persist. ,例如 persist.test。如果属性的开头为 ro,则该属性只能读,不能写,也就是 setprop 会无效。而如果没有特殊的开头,比如属性名仅仅是 test,那么该属性在重启之后将不会保留。

本篇博客到这里就差不多了,如果有任何错漏欢迎提出交流,谢谢。

    原文作者:mikechenmj
    原文地址: https://blog.csdn.net/weixin_37077539/article/details/88404401
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞