1. 程式人生 > >Android Hotfix 新方案——Amigo 原始碼解讀

Android Hotfix 新方案——Amigo 原始碼解讀

現在 hotfix 框架有很多,原理大同小異,基本上是基於qq空間這篇文章 或者微信的方案。可惜的是微信的 Tinker 以及 QZone 都沒有將其具體實現開源出來,只是在文章中分析了現有各個 hotfix 框架的優缺點以及他們的實現方案。Amigo 原理與 Tinker 基本相同,但是在 Tinker 的基礎上,進一步實現了 so 檔案、資原始檔、Activity、BroadcastReceiver 的修復,幾乎可以號稱全面修復,不愧 Amigo(朋友)這個稱號,能在危急時刻送來全面的幫助。

首先我們先來看看如何使用這個庫。

用法

   在 project 的build.gradle

 中

dependencies {
 classpath 'me.ele:amigo:0.0.3'
 } 

   在 module 的build.gradle 中

 apply plugin: 'me.ele.amigo'

   就這樣輕鬆的集成了 Amigo。

生效補丁包

   補丁包生效有兩種方式可以選擇:

  • 稍後生效補丁包

   如果不想立即生效而是使用者第二次開啟 App 時才打入補丁包,則可以將新的 Apk 放到 /data/data/{your pkg}/files/amigo/demo.apk,第二次開啟時就會自動生效。可以通過這個方法

  File hotfixApk = Amigo
.getHotfixApk(context);

   獲取到新的 Apk。
   同時,你也可以使用 Amigo 提供的工具類將你的補丁包拷貝到指定的目錄當中。
   

 FileUtils.copyFile(yourApkFile, amigoApkFile);
  • 立即生效補丁包

   如果想要補丁包立即生效,呼叫以下兩個方法之一,App 會立即重啟,並且打入補丁包。

    Amigo.work(context);
  Amigo.work(context, apkFile);

刪除補丁包

如果需要刪除掉已經下好的補丁包,可以通過這個方法

Amigo.clear(context);

提示

:如果apk 發生了變化,Amigo 會自動清除之前的apk。

自定義介面

在熱修復的過程中會有一些耗時的操作,這些操作會在一個新的程序中的 Activity 中執行,所以你可以通過以下方式來自定義這個 Activity。

<meta-data
 android:name="amigo_layout"
 android:value="{your-layout-name}" />

<meta-data
 android:name="amigo_theme"
 android:value="{your-theme-name}" />

元件修復

Amigo 目前能夠支援增加 Activity 和 BroadcastReceiver。只需要將新的 Activity 和 BroadcastReceiver 加到新的 Apk 包中就可以了。Service 和 ContentProvider 將會在未來的版本中支援更新。

整合 Amigo 十分簡單,但是明白 Amigo 的實現更加重要。

原始碼分析

Amigo這個類中實現了主要的修復工作。我們一起追追看,到底是怎樣的實現。

檢查補丁包

Amigo.java

...

if (demoAPk.exists() && isSignatureRight(this, demoAPk)) {
  SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_MULTI_PROCESS);
  String demoApkChecksum = checksum(demoAPk);
  boolean isFirstRun = !sp.getString(NEW_APK_SIG, "").equals(demoApkChecksum);
...

這段程式碼中,首先檢查是否有補丁包,並且簽名正確,如果正確,則通過檢驗校驗和是否與之前的檢驗和相同,不同則為檢測到新的補丁包。

釋放Apk

當這是新的補丁包時,首先第一件事就是釋放。ApkReleaser.work(this, layoutId, themeId)在這個方法中最終會去開啟一個 ApkReleaseActivity,而這個 Activity 的layout 和 theme 就是之前從配置中解析出來,在 work 方法中傳進來的layoutId 和 themeId。

ApkReleaseActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 ...

 new Thread() {
   @Override
   public void run() {
     super.run();

     DexReleaser.releaseDexes(demoAPk.getAbsolutePath(), dexDir.getAbsolutePath());
     NativeLibraryHelperCompat.copyNativeBinaries(demoAPk, nativeLibraryDir);
     dexOptimization();

     handler.sendEmptyMessage(WHAT_DEX_OPT_DONE);
   }
 }.start();
}

在 ApkReleaseActivity 的 onCreate() 方法中會開啟一個執行緒去進行一系列的釋放操作,這些操作十分耗時,目前在不同的機子上測試,從幾秒到二十幾秒之間不等,如果就這樣黑屏在使用者前面未免太不優雅,所以 Amigo 開啟了一個新的程序,啟動這個 Activity。
在這個執行緒中,做了三件微小的事情:

  • 釋放 Dex 到指定目錄
  • 拷貝 so 檔案到 Amigo 的指定目錄下   拷貝 so 檔案是通過反射去呼叫 NativeLibraryHelper這個類的nativeCopyNativeBinaries()方法,但這個方法在不同版本上有不同的實現。   
    • 如果版本號在21以下
          NativeLibraryHelper   
  public static int copyNativeBinariesIfNeededLI(File apkFile, File sharedLibraryDir) {
         final String cpuAbi = Build.CPU_ABI;
         final String cpuAbi2 = Build.CPU_ABI2;
         return nativeCopyNativeBinaries(apkFile.getPath(), sharedLibraryDir.getPath(), cpuAbi,
                 cpuAbi2);
     }

  會去反射呼叫這個方法,其中系統會自動判斷出 primaryAbi 和 secondAbi。

* 如果版本號在21以下

copyNativeBinariesIfNeededLI(file, file)這個方法已經被廢棄了,需要去反射呼叫這個方法

NativeLibraryHelper

  public static int copyNativeBinaries(Handle handle, File sharedLibraryDir, String abi) {
         for (long apkHandle : handle.apkHandles) {
             int res = nativeCopyNativeBinaries(apkHandle, sharedLibraryDir.getPath(), abi,
                     handle.extractNativeLibs, HAS_NATIVE_BRIDGE);
             if (res != INSTALL_SUCCEEDED) {
                 return res;
             }
         }
         return INSTALL_SUCCEEDED;
     }

所以首先得去獲得一個NativeLibraryHelper$Handle類的例項。之後就是找 primaryAbi。Amigo 先對機器的位數做了判斷,如果是64位的機子,就只找64位的 abi,如果是32位的,就只找32位的 abi。然後將 Handle 例項當做引數去呼叫NativeLibraryHelperfindSupportedAbi來獲得primaryAbi。最後再去呼叫copyNativeBinaries去拷貝 so 檔案。

對於 so 檔案載入的原理可以參考這篇文章

  • 優化 dex 檔案

ApkReleaseActivity.java

 private void dexOptimization() {
  ...
    for (File dex : validDexes) {
      new DexClassLoader(dex.getAbsolutePath(), optimizedDir.getAbsolutePath(), null, DexUtils.getPathClassLoader());
      Log.e(TAG, "dexOptimization finished-->" + dex);
    }
  }

DexClassLoader 沒有做什麼事情,只是呼叫了父類構造器,他的父類是 BaseDexClassLoader。在 BaseDexClassLoader 的構造器中又去構造了一個DexPathList 物件。
DexPathList類中,有一個 Element 陣列

DexPathList

 /** list of dex/resource (class path) elements */
 private final Element[] dexElements;

Element 就是對 Dex 的封裝。所以一個 Element 對應一個 Dex。這個 Element 在後文中會提到。

  優化 dex 只需要在構造 DexClassLoader 物件的時候將 dex 的路徑傳進去,系統會在最後會通過DexFile
  
  DexFile.java
  

 native private static int openDexFile(String sourceName, String outputName,
    int flags) throws IOException;

  
   來這個方法來載入 dex,載入的同時會對其做優化處理。
   
這三項操作完成之後,通知優化完畢,之後就關閉這個程序,將補丁包的校驗和儲存下來。這樣第一步釋放 Apk 就完成了。之後就是重頭戲替換修復。

替換修復

替換classLoader

Amigo 先行構造一個AmigoClassLoader物件,這個AmigoClassLoader是一個繼承於PathClassLoader的類,把補丁包的 Apk 路徑作為引數來構造AmigoClassLoader物件,之後通過反射替換掉 LoadedApk 的 ClassLoader。這一步是 Amigo 的關鍵所在。

替換Dex

之前提到,每個 dex 檔案對應於一個PathClassLoader,其中有一個 Element[],Element 是對於 dex 的封裝。

Amigo.java

private void setDexElements(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException {
 Object dexPathList = getPathList(classLoader);
 File[] listFiles = dexDir.listFiles();

 List<File> validDexes = new ArrayList<>();
 for (File listFile : listFiles) {
   if (listFile.getName().endsWith(".dex")) {
     validDexes.add(listFile);
   }
 }
 File[] dexes = validDexes.toArray(new File[validDexes.size()]);
 Object originDexElements = readField(dexPathList, "dexElements");
 Class<?> localClass = originDexElements.getClass().getComponentType();
 int length = dexes.length;
 Object dexElements = Array.newInstance(localClass, length);
 for (int k = 0; k < length; k++) {
   Array.set(dexElements, k, getElementWithDex(dexes[k], optimizedDir));
 }
 writeField(dexPathList, "dexElements", dexElements);
}

在替換dex時,Amigo 將補丁包中每個 dex 對應的 Element 物件拿出來,之後組成新的 Element[],通過反射,將現有的 Element[] 陣列替換掉。
在 QZone 的實現方案中,他們是通過將新的 dex 插到 Element[] 陣列的第一個位置,這樣就會先載入新的 dex ,微信的方案是下發一個 DiffDex,然後在執行時與舊的 dex 合成一個新的 dex。但是 Amigo 是下發一個完整的 dex直接替換掉了原來的 dex。與其他的方案相比,Amigo 因為直接替換原來的 dex ,相容性更好,能夠支援修復的方面也更多。但是這也導致了 Amigo 的補丁包會較大,當然,也可以發一個利用 BsDiff 生成的差分包,在本地合成新的 apk 之後再放到 Amigo 的指定目錄下。

替換動態連結庫

Amigo.java

private void setNativeLibraryDirectories(AmigoClassLoader hackClassLoader)
      throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException {
 injectSoAtFirst(hackClassLoader, nativeLibraryDir.getAbsolutePath());
 nativeLibraryDir.setReadOnly();
 File[] libs = nativeLibraryDir.listFiles();
 if (libs != null && libs.length > 0) {
   for (File lib : libs) {
     lib.setReadOnly();
   }
 }
}

so 檔案的替換跟 QZone 替換 dex 原理相差不多,也是利用 ClassLoader 載入 library 的時候,將新的 library 加到陣列前面,保證先載入的是新的 library。但是這裡會有幾個小坑。

DexUtils.java

public static void injectSoAtFirst(ClassLoader hackClassLoader, String soPath) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    Object[] baseDexElements = getNativeLibraryDirectories(hackClassLoader);
    Object newElement;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      Constructor constructor = baseDexElements[0].getClass().getConstructors()[0];
      constructor.setAccessible(true);
      Class<?>[] parameterTypes = constructor.getParameterTypes();
      Object[] args = new Object[parameterTypes.length];
      for (int i = 0; i < parameterTypes.length; i++) {
        if (parameterTypes[i] == File.class) {
          args[i] = new File(soPath);
        } else if (parameterTypes[i] == boolean.class) {
          args[i] = true;
        }
      }

      newElement = constructor.newInstance(args);
    } else {
      newElement = new File(soPath);
    }
    Object newDexElements = Array.newInstance(baseDexElements[0].getClass(), 1);
    Array.set(newDexElements, 0, newElement);
    Object allDexElements = combineArray(newDexElements, baseDexElements);
    Object pathList = getPathList(hackClassLoader);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      writeField(pathList, "nativeLibraryPathElements", allDexElements);
    } else {
      writeField(pathList, "nativeLibraryDirectories", allDexElements);
    }
  }

注入 so 檔案到陣列時,會發現在不同的版本上封裝 so 檔案的是不同的類,在版本23以下,是File

DexPathList.java

/** list of native library directory elements */
private final File[] nativeLibraryDirectories;

在23以上卻是改成了Element

DexPathList.java

/** List of native library path elements. */
private final Element[] nativeLibraryPathElements;

因此在23以上,Amigo 通過反射去構造一個 Element 物件。之後就是將 so 檔案插到陣列的第一個位置就行了。
第二個小坑是nativeLibraryDir要設定成readOnly。

DexPathList.java

public String findNativeLibrary(String name) {
   maybeInit();
   if (isDirectory) {
       String path = new File(dir, name).getPath();
       if (IoUtils.canOpenReadOnly(path)) {
           return path;
       }
   } else if (zipFile != null) {
       String entryName = new File(dir, name).getPath();
       if (isZipEntryExistsAndStored(zipFile, entryName)) {
         return zip.getPath() + zipSeparator + entryName;
       }
   }
   return null;
}

在ClassLoader 去尋找本地庫的時候,如果 so 檔案沒有設定成ReadOnly的話是會不會返回路徑的,這樣就會報錯了。

替換資原始檔

Amigo.java

...
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = getDeclaredMethod(AssetManager.class, "addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, demoAPk.getAbsolutePath());
setAPKResources(assetManager)
...

想要更新資原始檔,只需要更新Resource中的 AssetManager 欄位。AssetManager提供了一個方法addAssetPath。將新的資原始檔路徑加到AssetManager中就可以了。在不同的 configuration 下,會對應不同的 Resource 物件,所以通過 ResourceManager 拿到所有的 configuration 對應的 resource 然後替換其 assetManager。

替換原有 Application

相關推薦

Android Hotfix 方案——Amigo 原始碼解讀

現在 hotfix 框架有很多,原理大同小異,基本上是基於qq空間這篇文章 或者微信的方案。可惜的是微信的 Tinker 以及 QZone 都沒有將其具體實現開源出來,只是在文章中分析了現有各個 hotfix 框架的優缺點以及他們的實現方案。Amigo 原理與 Tink

Android下拉重新整理PullToRefresh原始碼解讀

我是Android新手,我就先打算閱讀優秀的開源專案來提高自己的水平,下面我將要把我自己解讀的Android下拉重新整理實現的基本步驟做一下我自己的解讀 學習資料來源:http://blog.csdn.net/leehong2005/article/details/1256

Netty-連線接入原始碼解讀

什麼是新連線接入?以及新連線接入前,Netty處於什麼狀態 netty的服務端NioServerSocketChannel初始化,註冊在BossGroup中的一條NioEventLoop中,並且給NioServerSocketChannel中維護的jdk原生的ServerSocketChannel繫結好了埠後

系統角度解讀Android P特性

引言2018年3月8日,谷歌釋出了Android P的預覽版,預計今年的Q3季度釋出final release版本,有不少文章從開發者角度介紹了Android P的新特徵,初步來看給感覺這次大版本似乎並沒有什麼改變。接下來,將從系統Treble,System,Framewor

Android中螢幕適配框架AutoLayout原始碼解讀

個人推薦 隨著越來越多的開發者使用hongyang大神的AutoLayout框架進行Android螢幕適配,我就去看了看這套框架做了哪些東西,結果發現AutoLayout框架本身程式碼量很少,但是思路值得我們開發者學習. 原始碼部落格地址 當我

原始碼解讀android 5.0控制元件TabLayout無法自定義下劃線寬度問題!

首先我要說的是TabLayout這個控制元件非常好用,隨便搜尋下網上一大堆的關於TabLayout的用法,因此我也就不具體介紹TabLayout的使用了。 這裡我們談談為什麼TabLayout無法自定義下劃線寬度問題,廢話不多說,上原始碼: 首先找遍原始

Android Handler訊息機制原始碼解讀

這個東西在網上已經被很多人寫過了,自己也看過很多文章,大概因為自己比較愚笨一直對此不太理解,最近重新從原始碼的角度閱讀,並且配合著網上的一些相關部落格才算明白了一些 本文從原始碼的角度順著程式碼的執行去原始碼,限於作者的表達能力及技術水平,可能會有些問題,請耐性

Cordova-android系列原始碼解讀(一)載入h5頁面流程

Cordova是一個比較成熟的跨跨平臺框架,核心思想就是Native提供h5容器,業務邏輯由h5處理,因為h5是直接跑在瀏覽器中的,既而達到跨平臺目的 本文旨在梳理cordovar第一個流程,在an

Anko:Android 程式碼動態佈局的方案

Anko版本的 hello world : Anko Layouts is a DSL for writing dynamic Android layouts. Here is a simple UI written with Anko DSL: ver

Android 7.0APK簽名方案-APK signature scheme v2

證書和金鑰庫 公鑰證書(也稱為數字證書或身份證書)包含公鑰/私鑰對的公鑰,以及可以標識金鑰所有者的一些其他元資料(例如名稱和位置)。證書的所有者持有對應的私鑰。 在您簽署 APK 時,簽署工具會將公鑰證書附加到 APK。公鑰證書充當“指紋”,用於將 APK 唯一關聯到您及您的對應私鑰。這有助於 Andr

Layout Inspector —— Android Studio 替代 Hierarchy Viewer 的方案

最近在研究 View 視窗機制的時候想要檢視一下應用的檢視結構,第一印象當然是佈局檢視神器 —— Hierarchy Viewer 啦!然後走進 /sdk/tools/ 目錄下發現曾經的 Hierarchyviewer.bat 不見了 —— 而原來的是這樣

Android訊息機制原始碼解讀

不要心急,一點一點的進步才是最靠譜的。 讀完本文你將瞭解: 前言 本來我以為自己很瞭解 Handler,在印象中 Android 訊息機制無非就是: Handler 給 MessageQueue 新增訊息 然後 Looper 無限迴圈讀取訊息 再呼叫 H

Hybrid----優秀開原始碼解讀之JS與iOS Native Code互調的優雅實現方案

轉載自:http://blog.csdn.net/yanghua_kobe/article/details/8209751 簡介 它優雅地實現了在使用UIWebView時JS與ios 的ObjC nativecode之間的互調,支援訊息傳送、接收、訊息

Android加入的視頻格式--媒體庫掃描

日期 ams bsp net gif nload static class mar 需求:在mediaprovider數據庫中加入.mov後綴格式的視頻文件 能夠使用工具MediaInfo_GUI_0.7.67_Windows.3243836749.exe 查看mov文

android或clean後R.java不見了怎麽處理

lean 自動生成 fix roi 自動構建 並且 tool 十個 ole R.java這個文件是會自動生成的。但是有時候你寫錯xml文件的時候,R.java是不會自動生成對應的值。這個時候我們會很習慣去clean一下這個項目,這個時候會突然發現,R.java竟然不見了。

Android O特性和行為變更總結zz

檢測 總結 提示 容易 使用情況 賬號 attr tube strac https://mp.weixin.qq.com/s/Ezfm-Xaz3fzsaSm0TU5LMw 1. Android O 新特性   前段時間解決了幾個 QQ 音樂多窗口屏幕顯示的 bug,雖然

2015年Android開發技術盤點

youtube har pro ner j2e bind cor compile -m 又到年末。 利用中午的時間,匯總盤點一下今年Android開發方面的新技術。感覺如今Android開發沒有曾經那麽純粹了,出現了非常多新的開發模式。2015年影響比較普遍的

vue單頁應用前進刷後退不刷方案探討

nested 規則 meta route 獲取 事先 ejs 啟用 ive 引言 前端webapp應用為了追求類似於native模式的細致體驗,總是在不斷的在向native的體驗靠攏;比如本文即將要說到的功能,native由於是多頁應用,新頁面可以啟用一個的新的webvie

yolo v2 損失函式原始碼解讀

前提說明:     1, 關於 yolo 和 yolo v2 的詳細解釋請移步至如下兩個連結,或者直接看論文(我自己有想寫 yolo 的教程,但思前想後下面兩個連結中的文章質量實在是太好了_(:з」∠)_)         yo

【React原始碼解讀】- 元件的實現

前言 react使用也有一段時間了,大家對這個框架褒獎有加,但是它究竟好在哪裡呢? 讓我們結合它的原始碼,探究一二!(當前原始碼為react16,讀者要對react有一定的瞭解) 回到最初 根據react官網上的例子,快速構建react專案 npx create-react-app