Android 开发基础 把 Android 应用当成命令行工具来用

codeskyblue · 2017年05月11日 · 最后由 我问问 回复于 2018年06月07日 · 1819 次阅读
本帖已被设为精华帖!

背景

这种方法的好处可以使用 shell 用户的权限调用 Android 系统中的一些特殊接口,比如模拟点击,屏幕截图。

命令行工具如何使用

比如一个 Android 的包名是 com.example.helloworld,首先拿到包在手机上的安装路径

$ adb shell pm path com.example.helloworld
# expect: package:/data/app/com.example.helloworld-1.apk

然后使用一个很长的命令调用启动 helloworld 中的 Console.java 中的代码

$ adb shell CLASSPATH=/data/app/com.example.helloworld-1.apk exec app_process /system/bin com.example.helloworld.Console --help
Usage of helloworld:
    -h, --help  print this message
    -v, --version show current version
    ... other ...

具体用法大概就是这样,接下来说下这个 Console.java 该怎么写

写一个支持命令行运行的 helloworld.apk

  1. 使用 AndroidStudio 创建一个名叫 HelloWorld 的应用。然后在有 MainActivity.java 文件的目录下,创建一个Console.java的文件。把下面的代码直接粘贴过去
package com.example.helloworld;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Created by hzsunshx on 2017/5/11.
 */

public class Console {
    private static final String PROCESS_NAME = "helloworld.cli";
    private static final String VERSION = "1.0";

    public static void main(String[] args) {
        setArgV0(PROCESS_NAME);

        for (String arg : args) {
            if (arg.equals("--help")) {
                System.out.println("TODO: Usage of " + PROCESS_NAME);
                return;
            } else if (arg.equals("--version")) {
                System.out.println(VERSION);
                return;
            } else {
                System.err.println("Error: unknown argument " + arg);
                System.exit(1);
            }
        }
    }

    private static void setArgV0(String text) {
        try {
            Method setter = android.os.Process.class.getMethod("setArgV0", String.class);
            setter.invoke(android.os.Process.class, text);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

这个代码比较简单,但是足够使用了。接下来用下外部的common-cli库,完善命令行的解析。

compile group: 'commons-cli', name: 'commons-cli', version: '1.3.1'加入到build.gradle文件中
main(String[] args)中的代码修改成

public static void main(String[] args) {
    setArgV0(PROCESS_NAME);

    Options options = new Options();
    options.addOption("v", "version", false, "show current version");
    options.addOption("h", "help", false, "show this message");

    CommandLineParser parser = new DefaultParser();
    HelpFormatter formatter = new HelpFormatter();
    CommandLine cmd;

    try {
        cmd = parser.parse(options, args);
    } catch (ParseException e) {
        System.err.println(e.getMessage());
        formatter.printHelp(PROCESS_NAME, options);
        System.exit(1);
        return;
    }

   if (cmd.hasOption("help")) {
        formatter.printHelp(PROCESS_NAME, options);
        return;
    }
    if (cmd.hasOption("version")) {
        System.out.println(VERSION);
        return;
    }
}

然后代码的上部加入import org.apache.commons.cli.*;

编译,然后安装到手机上,试试效果

$ adb shell CLASSPATH=/data/app/com.example.helloworld-2.apk exec app_process /system/bin com.example.helloworld.Console --help
usage: helloworld.cli
 -h,--help      show this message
 -v,--version   show current version

关于这个库的更详细的用法参考 https://commons.apache.org/proper/commons-cli/

结语

好久没写文章了,另一方面也说明我很久没看新的东西了,罪过罪过。赶紧擦干净书上的灰尘,倒上一杯龙井茶。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 30 条回复 时间 点赞

app_process 也能这么用 学习了 以前就想这个工具肯定能做点事情 你今天的分享给我提供了很好的自动化的思路

如果编写一个 apk 额外再提供一个命令行解析 用于自动化自身 貌似会很方便

思寒_seveniruby 将本帖设为了精华贴 05月11日 21:51

大牛过奖了

codeskyblue 回复

你试过 jar 包也能放进去 classpath 可以执行吗 我记得有本书提过好像 jar 也可以 不过我有点怀疑

没试过,这种方法,还是我从 stf 里面的代码翻出来的

这样不也行吗,不知道是不是同一个意思
adb shell am start -n package/activity -e key value -e key value
代码中是这样

Intent intent = getIntent();
String value = intent.getStringExtra(key);

再说我的这种方式的一个应用场景:
公司的 IP 经常发生变化,连接 fiddler 时,时不时需要手动更换 IP 很麻烦。

  • 于是写个 apk 来更换代理地址
  • 接受命令行参数输入
  • 通过查询本地 IP,再用脚本整合成 adb 命令启动这个 apk
  • 于是乎代理 IP 就改好了,方便多了

知道 stf 是采用这种方式,但是没有意识到可以提炼一种解决方案。赞楼主的总结归纳能力。👍 👍

Heyniu 回复

可以改天分享下. 听起来不错的小工具.

Heyniu 回复

你这是应用正常的启动方式,权限没有用 app_process 启动的大

然而没什么用武之地......多此一举,直接用命令行好了。
只是吧简单的东西复杂化。

jar 包是可以的, 将 jar 包转换为 dex 格式的 jar 就能执行, monkey 貌似就是这么做的

wuhao 回复

你果然不懂

特意去看了下 stf 的源码,终于大致了解了应用场景:

function install() {
      log.info('Checking whether we need to install STFService')
      return getPath()
        .then(function(installedPath) {
          log.info('Running version check')
          // 此处通过 adb 的 exec app_process 命令获取到 STFService 的 version 值,进而判断是否需要继续安装 STFService 
          return adb.shell(options.serial, util.format(
            "export CLASSPATH='%s';" +
            " exec app_process /system/bin '%s' --version 2>/dev/null"
          , installedPath
          , resource.main
          ))
          .timeout(10000)
          .then(function(out) {
            return streamutil.readAll(out)
              .timeout(10000)
              .then(function(buffer) {
                var version = buffer.toString()
                if (semver.satisfies(version, resource.requiredVersion)) {
                  return installedPath
                }
                else {
                  throw new Error(util.format(
                    'Incompatible version %s'
                  , version
                  ))
                }
              })
          })
        })
        .catch(function() {
          log.info('Installing STFService')
          // Uninstall first to make sure we don't have any certificate
          // issues.
          return adb.uninstall(options.serial, resource.pkg)
            .timeout(15000)
            .then(function() {
              return promiseutil.periodicNotify(
                  adb.install(options.serial, resource.apk)
                , 20000
                )
                .timeout(65000)
            })
            .progressed(function() {
              log.warn(
                'STFService installation is taking a long time; ' +
                'perhaps you have to accept 3rd party app installation ' +
                'on the device?'
              )
            })
            .then(function() {
              return getPath()
            })
        })

我理解这种方式最大的用处,是给一个相比 intent 更为隐秘的入口 (intent 毕竟比较公开) 。目前想到的应用场景,和 @heyniu 想的差不多,通过命令行设定一些内部参数,比如后端服务的 ip 地址(我们有多个环境),然后再启动 apk 。

有一些 android 系统为 app 提供了的接口 adb shell 没有现成命令使用,也可以这么用,例如 appium 里面经典的获取当前网络状态(这个系统接口貌似只有 app 才能调用,adb shell 调用不了)。

萤火虫 回复

那本质也是 dex 我也是觉得只有 dex 才能运行 之前看过一个韩国人写的内核的书 提到用 jar 我当时就怀疑 估计也就是把 dex 打包成 jar 了

Heyniu 回复

感谢分享。也写过简单的 apk 预装到手机里面辅助测试,但是仅仅知道怎么去把参数通过 adb shell 传给 apk。请问怎么获取 apk 的返回值?

看起来 am 命令就是调用了 app_process。

shell@zx55q05_64:/ $ cat /system/bin/am
#!/system/bin/sh
#
# Script to start "am" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"

@codeskyblue ,请教一下这里是不是需要 root 权限?我照着步骤运行命令,没有任何返回。

Kun 回复

原来 am 是这么来的,不用 root 也可以

以前(挺以前的事情了),可以直接在 shell 里面调用 dalvikvm 然后传给他 bootclasspath (framework.jar 那群人) 以及一个 apk/zip/jar(其实都一样)里面有一个编译好的 dex 文件,然后传给它一个包名,main 函数就启动了,dalvikvm 这个 exe 文件,和 jdk 标准的 java 是一样的 class path, main-class-name。命令行的区别在于,这个进程是一个 linux 进程 userid 和权限是 adb,没有/data path,属于杀鸡取卵。。。。另外一点,adb 权限比 app 权限要高,可以用来做坏事👽

codeskyblue 回复

am 这类,需要 adb 权限(也就是 adb shell 默认的用户)。app 默认通过 getRuntime().exec() 什么打开的,是 app 的 uid 的 shell,权限比较低

Heyniu 回复

看来我短期任务是让你换掉 fiddle 😼

AppetizerIO 回复

怎么说

Kun 回复

最简单的办法是 apk 的值写在 sdcard 某个目录,然后 adb 命令再去读出来

陈恒捷 回复

非常同意,有些命令 adb 是搞不定的,需要 app 才能获取到,这时作用就体现出来了

am 不是广播吗?不是应该比这种更方便吗?权限各种可控。
我没记错 am --user 0 也可以用 shell 权限运行啊。不需要 root

为什么我执行时报 ClassNotFoundException 异常:

C:\Users\3020> adb shell CLASSPATH=/data/app/com.example.helloworld-1/base.apk exec app_process /system/bin com.example.helloworld.Console --help
java.lang.ClassNotFoundException: Didn't find class "com.example.helloworld.Console" on path: DexPathList[[zip file "/data/app/com.example.helloworld-1/base.apk"],nativeLibraryDirectories=[/vendor/lib
64, /system/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
Suppressed: java.lang.ClassNotFoundException: com.example.helloworld.Console
at java.lang.Class.classForName(Native Method)
at java.lang.BootClassLoader.findClass(ClassLoader.java:781)
at java.lang.BootClassLoader.loadClass(ClassLoader.java:841)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
... 1 more
Caused by: java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available

27楼 已删除
陈恒捷 回复

不是更隐秘的意思,这样应用是比 root 更 root 的权限

去年翻到的命令行设置 WIFI 代理的工具:https://github.com/jpkrause/AndroidProxySetter 这个用 intent 实现传参的,不过改改也可以用楼主说的那种方式。😁 😁 😁 还有个想法,是不是可以直接打包成类似 am 的那种 dex 的 jar 包,推到手机用 app_process 来调用,这个可以避免安装带来的麻烦(OV 系的机器安装 apk 很蛋疼)

楼主,你好,
使用如下命令可以在 shell 下面用 minicap 截图,然后我想知道使用你提供的这种方法是否可以?能麻烦你帮忙试下吗?感谢

LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0 -s > /mnt/sdcard/tmp.jpg
bauul 回复

好使

33楼 已删除

这是干什么用的,大神

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册