目的:
1.查看一下流程
2.看一下关于签名的问题
3.看一下遍历的方式
反编译 从零到放弃
用到的反编译工具:jd-gui(mac版本)
下载完成后,在安全偏好设置里面,允许运行
将crawl_launcher.jar拉入打开即可
直接贴代码,主要的启动都在CrawLaucher.class 导入import的代码 没有放上来
可以先看一下代码,然后一脸懵的回来看一下,我个人理解的大概流程
launchCrawl:
定义了类对象crawlSetup,AppPackageNameExtractor,apkSigner,crawlerRepacker,adbExecutor
crawlSetup类主要是一些参数处理,和提到了三个内部用到的工具adb,aapt,zipalign
处理的参数主要在命令行调起appcrawler时候添加的参数
举个栗子:java -jar crawl_launcher.jar --android-sdk /Users/username/Library/Android/sdk--app-package-name com.xxx.xxxx--timeout-sec 600--ui-automator-mode
标红的就是我们添加的附加的参数,有些参数是必须填写的,有些是不必须的
这里对这些参数进行了很多官网已经说明的判断:
1.appPackageName,apkFilePath都没有填写直接抛出异常提示错误
2.deviceName,deviceSerialCode同时填写以deviceName为准
3.检测你填写的apkFilePath是否真的存在,且对应的apk是否真的存在,不存在抛出异常和异常提示信息
4.textGuide,roboScript,experimentsFile(可选参数,官网并没有给出说明),如果指定了上面文件的路径,路径和文件是否存在,不存在抛出异常和异常提示信息
5.判断上面三个文件和文件都正常存在的情况,覆盖一下对应的三个配置文件,使用你指定的文件
6.看是否指定了output(就是测试结果输出的路径,参数--output-dir控制),没有该参数就使用默认的output路径
AppPackageNameExtractor类主要是针对提供了apkFilePath后如何获得appPackageName来进行操作
主要通过aapt工具来完成packageName的获取,如果获取失败就抛出异常和异常提示信息
aapt工具如何获取packageName详情见aapt工具分享
ApkSigner类主要是用来完成签名的功能
这里主要用到了工具zipalign,前提是你提供了密钥库参数--key-store
CrawlerRepacker类根据上面的很多重新定义的文件和参数来对crawler进行重新打包
AdbExecutor类,通过这个类名字应该也联想到了,这是一个很多adb操作的类
1.获取and验证你的device是否正确,通过getDeviceSerialCode方法(传入两个参数:deviceName,deviceSerialCode)
判断的方法跟我们日常的判断方法基本一致,“adb devices -l" 然后对比是否
分几种情况:
a.你没有用参数--device-name和--device_serial-code但是发现有多个device,抛出异常和异常提示信息
b.你提供的devicename没有匹配到
c.你提供的devicename匹配到多个(这个地方一定有童鞋一问,我们的deviceName不是唯一么,为什么会匹配到多个 我们输入adb devices -l看一下

前面的serial-code是唯一的没有错,但是后面的model:RMX1825只得就是手机的型号当然可能重复,前面也提到了,是优先匹配deviceName的,除非没有该参数
所以这时候也抛出异常和异常提示信息
d.你提供的deviceSerialCode没有匹配到任何device,抛出异常
e.当前就没有链接任何的device设备,抛出异常
2.获取你的android版本信息,通过getDeviceApiLevel方法
这里也有个异常判断 因为通过的就是 adb shell get prop ro.build.version.sdk
如果没有拿到就抛出了异常认为你的device链接中断了
3.唤醒device,通过wakeDevice()方法
其实就是adb shell input key event 26
4.安装app,installApp方法
其实就是adb install -g
5.执行adb命令 execute方法
看代码应该不难看出来,这个方法开始就指定了 adb devicesName -s 后面启动一个进程,然后就是一些通过input读取命令然后执行,监控每一步的执行操作,如果有异常就捕获异常信息
转换成str后返回异常信息
这里就顺带说了另一个方法convertAdbCommandArgsToString就是用来转换adb返回的信息的
我们下面可以看主方法lauchCrawl
1.首先获取packageName
String appPackageName = this.crawlSetup.getAppPackageName();
if (appPackageName.isEmpty()) {
appPackageName = this.appPackageNameExtractor.extractAppPackageName();
}
2.唤醒device
if (this.crawlSetup.isWakeDevice()) { this.adbExecutor.wakeDevice(); }
3.定义了两个输出目录,开始前清空这两个目录
/crawl_outputs.proto
/crawl_outputs.txt
1 if (!isHostGuidedCrawl) { 2 FileUtil.cleanupDirectory(this.crawlSetup.getOutputDirectoryPath()); 3 }
4 这里就是涉及到签名的地方了 通过代码不难发现有两个方法能避免签名的限制
如果你添加的参数不是packageName是apkPath且你没有定义uiautomatorMode和instantAppsMode就会进行签名验证
这两个参数分别是--ui-automator-mode,--instant-apps-mode
举个栗子:
java -jar crawl_launcher.jar --android-sdk /Users/liuyiwen/Library/Android/sdk --app-package-name com.kwai.video--timeout-sec 600 --ui-automator-mode
如此使用就不会因为签名问题而造成失败
1 Optional<Path> appApkFilePath = Optional.empty(); 2 if (!this.crawlSetup.getApkFilePath().isEmpty()) { 3 appApkFilePath = Optional.of(Paths.get(this.crawlSetup.getApkFilePath(), new String[0])); 4 if (!this.crawlSetup.isUiAutomatorMode() && !this.crawlSetup.isInstantAppsMode()) { 5 appApkFilePath = Optional.of(this.apkSigner.sign((Path)appApkFilePath.get())); 6 } 7 } 8
首先引起我们注意的就的就是adb.Executor.execute 这个执行了 adb shell pm grant注意这个是对应用进行授权,但是这个只有api级别23以上才可以,所以会对this.deviceApiLevel>=23进行判断
5.app的安装,如果你填写的是apkFilePath
然后我们从头看,他还调用了cleanupDevice (appPackageName) 方法
这个方法首先通过adb shell am force-stop 停止了应用 然后通过adb shell rm -rf 清空了filepath然后卸载了应用androidx.test.tools.crawler 最后注意如果你提供的是filepath的话 他会把这个开始安装的测试的应用
最后也卸载掉。仿佛你没有测试过一样
1 installCrawlerAndApp(appPackageName, signedRepackedCrawlerApkFilePath, appApkFilePath); 2 3 4 =====================方法源代码=================== 5 private void installCrawlerAndApp(String appPackageName, Path crawlerApkFilePath, Optional<Path> appApkFilePath) { 6 cleanupDevice(appPackageName); 7 8 boolean grantPermissionsOnInstall = (this.deviceApiLevel >= 23); 9 10 this.adbExecutor.installApp(grantPermissionsOnInstall, new String[] { crawlerApkFilePath 11 .toAbsolutePath().toString() }); 12 13 14 this.adbExecutor.execute(new String[] { "shell", "pm", "grant", "androidx.test.tools.crawler", "android.permission.DUMP" }); 15 16 this.adbExecutor.installApp(grantPermissionsOnInstall, new String[] { "-r", this.crawlSetup 17 .getCrawlerStubappApk().getAbsolutePath() }); 18 19 if (appApkFilePath.isPresent()) { 20 this.adbExecutor.installApp(grantPermissionsOnInstall, new String[] { ((Path)appApkFilePath 21 .get()).toAbsolutePath().toString() }); 22 } 23 } 24 ================================================= 25 private void cleanupDevice(String appPackageName) { 26 this.adbExecutor.execute(new String[] { "shell", "am", "force-stop", appPackageName }); 27 this.adbExecutor.execute(new String[] { "shell", "rm", "-rf", "/sdcard/app_firebase_test_lab" }); 28 this.adbExecutor.execute(new String[] { "shell", "rm", "-rf", "/sdcard/robo_tmp_files" }); 29 this.adbExecutor.uninstallApp("androidx.test.tools.crawler"); 30 31 if (!this.crawlSetup.getApkFilePath().isEmpty()) { 32 this.adbExecutor.uninstallApp(appPackageName); 33 }34 }
6.log的配置,一些准备阶段的内容
1 LogcatRecorder logcatRecorder = null; 2 if (!isHostGuidedCrawl) { 3 if (this.crawlSetup.isPauseBeforeCrawl()) { 4 Logger.atInfo().log("Press Enter to start the crawl", new Object[0]); 5 try { 6 System.in.read(); 7 } catch (Exception exception) {} 8 } 9 10 11 12 13 logcatRecorder = new LogcatRecorder(this.adbExecutor, this.crawlSetup.getOutputDirectoryPath(), appPackageName); 14 logcatRecorder.start(); 15 (new VideocatRecorder(this.adbExecutor, this.crawlSetup, appPackageName, this.deviceApiLevel)).start(); 16 }
7.开始运行appCrawler
1 startCrawler(appPackageName)
8.最后调用的两个方法
就是输出log的两个方法 还包括等待30s来输出log等就不做详细介绍了
1 this.adbExecutor.stopReadingInput(); 2 3 if (!isHostGuidedCrawl) { 4 processLogcat(logcatRecorder); 5 processCrawlOutput();
下面就是全部的代码
public class CrawlLauncher
{
private final CrawlSetup crawlSetup;
private final AppPackageNameExtractor appPackageNameExtractor;
private final ApkSigner apkSigner;
private final CrawlerRepacker crawlerRepacker;
private final AdbExecutor adbExecutor;
private final int deviceApiLevel;
public CrawlLauncher(String[] crawlParameters) {
this.crawlSetup = CrawlSetupProvider.getCrawlSetup();
this.crawlSetup.processCrawlParameters(crawlParameters);
this.appPackageNameExtractor = new AppPackageNameExtractor(this.crawlSetup);
this.apkSigner = new ApkSigner(this.crawlSetup);
this.crawlerRepacker = new CrawlerRepacker(this.crawlSetup, this.apkSigner);
this.adbExecutor = new AdbExecutor(this.crawlSetup);
this.deviceApiLevel = this.adbExecutor.getDeviceApiLevel();
}
public static void main(String[] args) { (new CrawlLauncher(args)).launchCrawl(false); }
public CrawlSetup getCrawlSetup() { return this.crawlSetup; }
public AdbExecutor getAdbExecutor() { return this.adbExecutor; }
public void launchCrawl(boolean isHostGuidedCrawl) throws ApkSigningException, CrawlerRepackingException {
String appPackageName = this.crawlSetup.getAppPackageName();
if (appPackageName.isEmpty()) {
appPackageName = this.appPackageNameExtractor.extractAppPackageName();
}
Logger.atInfo().log("Preparing to crawl %s", new Object[] { appPackageName });
if (this.crawlSetup.isWakeDevice()) {
this.adbExecutor.wakeDevice();
}
if (!isHostGuidedCrawl) {
FileUtil.cleanupDirectory(this.crawlSetup.getOutputDirectoryPath());
}
this.crawlSetup.buildCrawler();
Path signedRepackedCrawlerApkFilePath = (this.crawlSetup.isUiAutomatorMode() || this.crawlSetup.isInstantAppsMode()) ? this.crawlSetup.getCrawlerAppApkPath() : this.crawlerRepacker.repackAndSignCrawlerApp(appPackageName);
Optional<Path> appApkFilePath = Optional.empty();
if (!this.crawlSetup.getApkFilePath().isEmpty()) {
appApkFilePath = Optional.of(Paths.get(this.crawlSetup.getApkFilePath(), new String[0]));
if (!this.crawlSetup.isUiAutomatorMode() && !this.crawlSetup.isInstantAppsMode()) {
appApkFilePath = Optional.of(this.apkSigner.sign((Path)appApkFilePath.get()));
}
}
installCrawlerAndApp(appPackageName, signedRepackedCrawlerApkFilePath, appApkFilePath);
LogcatRecorder logcatRecorder = null;
if (!isHostGuidedCrawl) {
if (this.crawlSetup.isPauseBeforeCrawl()) {
Logger.atInfo().log("Press Enter to start the crawl", new Object[0]);
try {
System.in.read();
} catch (Exception exception) {}
}
logcatRecorder = new LogcatRecorder(this.adbExecutor, this.crawlSetup.getOutputDirectoryPath(), appPackageName);
logcatRecorder.start();
(new VideocatRecorder(this.adbExecutor, this.crawlSetup, appPackageName, this.deviceApiLevel)).start();
}
startCrawler(appPackageName);
this.adbExecutor.stopReadingInput();
if (!isHostGuidedCrawl) {
processLogcat(logcatRecorder);
processCrawlOutput();
}
}
private void processLogcat(LogcatRecorder logcatRecorder) {
try {
logcatRecorder.join();
LogcatAnalyser analyser = new LogcatAnalyser();
ImmutableList immutableList = analyser.processLogcat(new BufferedReader(new InputStreamReader(new ByteArrayInputStream(logcatRecorder
.getLogcat()), StandardCharsets.UTF_8)), false);
long numOfNonSdkFindings = immutableList.stream().filter(finding -> (finding.type() == LogcatFinding.LogcatFindingType.NON_SDK_API_USED)).count();
if (numOfNonSdkFindings > 0L) {
Logger.atInfo().log("WARNING: Found %d usages of non-SDK API methods. See Logcat for details.", new Object[] {
Long.valueOf(numOfNonSdkFindings)
});
}
Optional<LogcatFinding> crash = immutableList.stream().filter(f -> (f.type() == LogcatFinding.LogcatFindingType.FATAL_EXCEPTION || f.type() == LogcatFinding.LogcatFindingType.NATIVE_CRASH)).findAny();
if (!crash.isPresent()) {
Logger.atInfo().log("SUCCESS: Found 0 crashes.", new Object[0]);
} else {
Logger.atInfo().log("FAILED: Found 1 crash.", new Object[0]);
Logger.atInfo().log(((LogcatFinding)crash.get()).message(), new Object[0]);
if (((LogcatFinding)crash.get()).stacktrace().isPresent()) {
Logger.atInfo().log((String)((LogcatFinding)crash.get()).stacktrace().get(), new Object[0]);
}
}
} catch (InterruptedException|java.io.IOException e) {
Logger.atWarning().withCause(e).log("Error while processing logcat.", new Object[0]);
}
}
private void installCrawlerAndApp(String appPackageName, Path crawlerApkFilePath, Optional<Path> appApkFilePath) {
cleanupDevice(appPackageName);
boolean grantPermissionsOnInstall = (this.deviceApiLevel >= 23);
this.adbExecutor.installApp(grantPermissionsOnInstall, new String[] { crawlerApkFilePath
.toAbsolutePath().toString() });
this.adbExecutor.execute(new String[] { "shell", "pm", "grant", "androidx.test.tools.crawler", "android.permission.DUMP" });
this.adbExecutor.installApp(grantPermissionsOnInstall, new String[] { "-r", this.crawlSetup
.getCrawlerStubappApk().getAbsolutePath() });
if (appApkFilePath.isPresent()) {
this.adbExecutor.installApp(grantPermissionsOnInstall, new String[] { ((Path)appApkFilePath
.get()).toAbsolutePath().toString() });
}
}
private void cleanupDevice(String appPackageName) {
this.adbExecutor.execute(new String[] { "shell", "am", "force-stop", appPackageName });
this.adbExecutor.execute(new String[] { "shell", "rm", "-rf", "/sdcard/app_firebase_test_lab" });
this.adbExecutor.execute(new String[] { "shell", "rm", "-rf", "/sdcard/robo_tmp_files" });
this.adbExecutor.uninstallApp("androidx.test.tools.crawler");
if (!this.crawlSetup.getApkFilePath().isEmpty()) {
this.adbExecutor.uninstallApp(appPackageName);
}
}
private void startCrawler(String appPackageName) {
List<String> executionOptions = getExecutionOptions(appPackageName);
List<String> startServiceCommand = new ArrayList<String>(Arrays.asList(new String[] { "shell", "am", "startservice" }));
startServiceCommand.addAll(executionOptions);
startServiceCommand.add("-n");
startServiceCommand.add("androidx.test.tools.crawler/androidx.test.tools.crawler.controller.CrawlDriver");
this.adbExecutor.execute(startServiceCommand);
List<String> instrumentCommand = new ArrayList<String>(Arrays.asList(new String[] { "shell", "am", "instrument", "--no-window-animation", "-w", "-r" }));
instrumentCommand.addAll(executionOptions);
addExecutionOption("class", "androidx.test.tools.crawler.CrawlPlatform", instrumentCommand);
instrumentCommand.add("androidx.test.tools.crawler/androidx.test.runner.AndroidJUnitRunner");
Logger.atInfo().log("Crawl started.", new Object[0]);
this.adbExecutor.execute(instrumentCommand);
this.adbExecutor.execute(new String[] { "shell", "am", "instrument", "-w", "-r", "androidx.test.tools.crawler/.CrawlMonitor" });
Logger.atInfo().log("Crawl finished.", new Object[0]);
}
private List<String> getExecutionOptions(String appPackageName) {
List<String> options = new ArrayList<String>();
addExecutionOption("crawlDurationSec",
String.valueOf(this.crawlSetup.getCrawlTimeoutSeconds()), options);
addExecutionOption("appPackageName", appPackageName, options);
addExecutionOption("appListener", "androidx.test.tools.crawler.SignaturePatchingCallback", options);
addExecutionOption("disableAnalytics", "true", options);
addExecutionOption("executionId", UUID.randomUUID().toString(), options);
addExecutionOption("dataDir", this.crawlSetup
.getOutputDirectoryPath().toAbsolutePath().toString(), options);
if (this.crawlSetup.isTestAccessibility()) {
addExecutionOption("testAccessibility", "true", options);
}
if (this.crawlSetup.isInstantAppsMode()) {
addExecutionOption("instantAppsMode", "true", options);
}
addExecutionOption("crawlDriverDaggerInitializerClassName", this.crawlSetup
.getCrawlDriverInitializerClass(), options);
Properties experiments = this.crawlSetup.getExperiments();
for (String experimentName : experiments.stringPropertyNames()) {
addExecutionOption(experimentName, experiments.getProperty(experimentName), options);
}
return options;
}
private static void addExecutionOption(String optionName, String optionValue, List<String> options) {
options.add("-e");
options.add(optionName);
options.add(optionValue);
}
private void processCrawlOutput() {
this.adbExecutor.execute(new String[] { "pull", "/sdcard/app_firebase_test_lab", this.crawlSetup
.getOutputDirectoryPath().toAbsolutePath().toString() });
Optional<File> crawlOutputsBinaryProtoFile = getCrawlOutputsBinaryProtoFile();
if (crawlOutputsBinaryProtoFile.isPresent()) {
Logger.atInfo().log("Converting output files", new Object[0]);
try { CrawlOutputsProto.CrawlOutputs crawlOutputs = CrawlOutputsProto.CrawlOutputs.parseFrom(new FileInputStream((File)crawlOutputsBinaryProtoFile.get()));
Writer fileWriter = Files.newBufferedWriter(Paths.get(this.crawlSetup.getCrawlOutputsTextProtoPath(), new String[0]), StandardCharsets.UTF_8, new java.nio.file.OpenOption[0]);
try { TextFormat.print(crawlOutputs, fileWriter);
if (fileWriter != null) fileWriter.close(); } catch (Throwable throwable) { if (fileWriter != null)
try { fileWriter.close(); } catch (Throwable throwable1) { throwable.addSuppressed(throwable1); } throw throwable; } } catch (Exception e)
{ throw new RuntimeException("Failed to convert output files", e); }
}
Logger.atInfo().log("The output directory is %s", new Object[] { this.crawlSetup
.getOutputDirectoryPath().toAbsolutePath().toString() });
}
private Optional<File> getCrawlOutputsBinaryProtoFile() {
File crawlOutputsBinaryProtoFile = new File(this.crawlSetup.getCrawlOutputsBinaryProtoPath());
int waitForCrawlOutputsAttempts = 30;
while (!crawlOutputsBinaryProtoFile.exists() && waitForCrawlOutputsAttempts > 0) {
Logger.atDebug().log("Waiting for crawl outputs proto file %s", new Object[] { crawlOutputsBinaryProtoFile });
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Logger.atWarning().log("Interrupted while waiting for outputs proto file %s", new Object[] { crawlOutputsBinaryProtoFile });
}
waitForCrawlOutputsAttempts--;
}
if (!crawlOutputsBinaryProtoFile.exists()) {
Logger.atWarning().log("Timed out waiting for crawl outputs proto file %s", new Object[] { crawlOutputsBinaryProtoFile });
return Optional.empty();
}
return Optional.of(crawlOutputsBinaryProtoFile);
}
}
|
