apkanalyzer(2)-命令实现基本结构

熟悉了apkanalyzer的基本功能,接下来就看下这个工具的具体实现了。本文主要根据命令执行的基本流程,尝试理解它实现的基本结构。

ANDROID_SDK_HOME/tools/bin/apkanalyzer

apkanalyzer文件是个可执行的shell脚本,可以视作是这个apk分析工具的命令行执行文件。查看shell源码,可以看出其中很大部分是针对不同的系统环境进行检查,拼接最终的执行指令。主要看最后几行执行命令:

1
2
3
4
5
6
7
8
9
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $APKANALYZER_OPTS -classpath "\"$CLASSPATH\"" com.android.tools.apk.analyzer.ApkAnalyzerCli "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

相关的变量定义基本都可以搜到。可以确定最后是使用java指令,执行了com.android.tools.apk.analyzer.ApkAnalyzerCli 的main函数方法。

apkanalyzer**.jar

按图索骥之下,ApkAnalyzerCli也是很难不被发现的。

在sdk tools目录下,会看到很多耳熟能详的名字,android、lint、monkeyruuner、sdkmanager和uiautomatorviewer等等。一些非常熟悉的AS功能的底层支持,似乎也有了一些解释。图中只截取了bin和lib两个文件夹,bin中一般放一些可执行的shell脚本;lib里面则是一些支持相关功能的jar包。一眼望去,就可以找到apkanalyzer的相关文件。

apkanalyzer的两个jar包文件虽然只有几十K,但是其中的引用可是不少。能够望文生义的一些依赖库就有xml、注释、smali、布局相关的解析库。

那么,这个工具的基本结构就可以确定了。

  • bin目录下的apkanalyzer,作为一个命令实现,封装了apkanalyzer jar功能的入口、整合输入参数,并实现了环境检查;
  • apkanalyzer-cli.jar 类似于一个可执行jar包,工具的main在这里定义;
  • apkanalyzer.jar 基本上就是各个功能分析点的实现的流程封装;
  • 各种其他被依赖进来的jar 具体某个功能点相关文件、功能解析工具。

入口代码简析

既然是sdk的tools目录下的代码分析,自然可以找源码来看。当然如果只想搞清楚一些功能原理,直接看jar包也是可以的。实践中,使用JD-GUI经常会报错:

后来使用Luyten,就可以解析出源码了。

cli jar中只有两个类:

  • ApkAnalyzerCli,入口类
  • ApkAnalyzerImpl,功能实现类

各六百多行。从ApkAnalyzerCli这个入口类的静态常量定义中可以找到很多文档中提到的命令功能参数。首先可以看一下入口类的主要成员变量、构造器和main方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private final PrintStream out;
private final PrintStream err;
private final ApkAnalyzerImpl impl;
public ApkAnalyzerCli(final PrintStream out, final PrintStream err, final ApkAnalyzerImpl impl) {
this.out = out;
this.err = err;
this.impl = impl;
}
public static void main(final String[] args) {
final ApkAnalyzerCli instance = new ApkAnalyzerCli(System.out, System.err, new ApkAnalyzerImpl(System.out, getAaptInvokerFromSdk(null)));
instance.run(args);
}

可以简单看出,入口类跟功能实现类是一个组合关系,入口类持有一个功能实现类的实体。在主函数中,初始化入口类实体,执行入口类run函数;至于功能实现类的构造后面再聊。

run函数稍微有些长,可以看出处理参数解析和异常处理之外,有一个核心的枚举类Action,核心功能就是执行Action类型的execute函数。贴一部分该类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Action
{
APK_SUMMARY("apk", "summary", "Prints the application Id, version code and version name.") {
@Override
public void execute(final PrintStream out, final PrintStream err, final ApkAnalyzerImpl impl, final String... args) {
final OptionParser parser = this.getParser();
final OptionSet opts = parseOrPrintHelp(parser, err, args);
impl.apkSummary(((File)opts.valueOf((OptionSpec)this.getFileSpec())).toPath());
}
},
//...
private Action(final String subject, final String verb, final String description) {
this.subject = subject;
this.verb = verb;
this.description = description;
}
public abstract void execute(final PrintStream p0, final PrintStream p1, final ApkAnalyzerImpl p2, final String... p3);
}

可以看出,所谓Action,就是每个分析功能的抽象。具体到实现方式,是使用枚举类型、增加构造器参数、属性成员和抽象方法,提供一个静态方法findActions,通过参数分析实例化一个枚举实体出来,在功能代码里调用execute来执行分析。当然,具体上来说还有具体某个分析功能的具体参数解析和异常处理、参数缓存之类的一揽子实现,不甚复杂。

ApkAnalyzerCli的构造器和Action的execute方法都涉及到了cli包里面的另外一个类——ApkAnalyzerImpl。具体到每个Action的execute方法实现中,可以看到最后一步基本上都是调用了ApkAnalyzerImpl的某个功能方法。那么,看一下该类的构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.stream.*;
import com.google.devrel.gmscore.tools.apk.arsc.*;
import org.jf.dexlib2.dexbacked.*;
import java.nio.file.*;
import com.google.common.base.*;
import com.android.tools.proguard.*;
import com.google.common.collect.*;
import com.android.tools.apk.analyzer.dex.tree.*;
import java.util.*;
import com.android.tools.apk.analyzer.dex.*;
import java.util.function.*;
import com.android.ide.common.xml.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import com.android.ide.common.process.*;
import javax.swing.tree.*;
import com.android.tools.apk.analyzer.internal.*;
import com.google.common.io.*;
import java.io.*;
import java.text.*;
public class ApkAnalyzerImpl
{
private final PrintStream out;
private final AaptInvoker aaptInvoker;
private boolean humanReadableFlag;
public ApkAnalyzerImpl(final PrintStream out, final AaptInvoker aaptInvoker) {
this.out = out;
this.aaptInvoker = aaptInvoker;
}
//...
}

从它的包引用和成员属性来看,这个实现类需要一个aapt工具,包引用中又涉及到了其他工具包,如dex相关、arsc相关、xml相关。单从构造器方法来讲,主要是AaptInvoker的传入,回看到入口类的main函数,AaptInvoker是通过一个静态方法【getAaptInvokerFromSdk】获取的。通过这个静态方法,基本上可以看到各种属性的sdk目录。那么AaptInvoker在哪呢?答案是在analyzer.jar中。

aapt是什么应该不需要多说,粗略来看这个所谓aapt执行类,通过参数AndroidSdkHandler定位到aapt工具的位置,同时引入了com.android.ide.common.process工具包中的进程执行工具,来实现一些解析功能:

public List getXmlTree(File apk, String xmlResourcePath)

public List dumpBadging(File apk)

AndroidSdkHandler 类在sdklib-26.0.0-dev.jar工具包中,从该类的方法列表上看应该是sdk环境中工具的一个管理类,稍微贴几个函数名感受一下。

getSystemImageManager

getSdkManager

getAndroidTargetManager

getLatestBuildTool

另外一对工具类,是在sdk-common-26.0.0-dev.jar工具包中,从包名目录上看,是ide打包使用的相关工具类。

那么,根据上面简单的走读,基本可以确定分析工具的基本结构。从shell脚本,链接到cli jar包中的入口类,再具体调用到apkanalyzer.jar的各种工具方法。

小结

  • 学习到工具脚本的封装方法,调用链路的设计方式
  • SDK是一个宝库,永远要知道没有IDE工具,单纯的使用SDK也是可以完成Android 开发的,那么SDK里面应该有哪些东西就不言而喻了
  • 一个程序猿的知识广度,基本可以决定在一个业务问题摆在他面前时,是否有足够的知识储备来支撑想象力从而提出解决方案。所以技术的理论知识和实现细节,是需要通过这种走读来补充的。

后面会取几个基本的功能命令,走读下具体的解析实现。

感谢您赏个荷包蛋~