Google AppCrawler反編譯-粗略的個人理解


目的:

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);
  }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM