【熱修復】Andfix源碼分析

轉載請標註來源:http://www.cnblogs.com/charles04/p/8471301.htmlhtml


 Andfix源碼分析

0、目錄

  1. 背景介紹
  2. 源碼分析
  3. 方案評價
  4. 總結與思考
  5. 參考文獻

一、背景介紹

熱修復技術是移動端領域近年很是活躍的一項新技術,經過熱修復技術能夠在不發佈應用市場版本,在用戶無感知的狀況下對線上Bug進行緊急修復。正所謂修復於千里以外,剿滅與無形之中,實乃移動端開發運營中一項必備之尖端技術。其主要的運行原理以下: android

簡而言之,熱修復就是經過必定的技術手段,讓用戶在程序的實際運行操做中,走到修復的Patch邏輯序列,而繞開存在問題的邏輯片斷,實現問題的緊急規避。目前實現的技術手段主要有騰訊系的基於ClassLoader的熱修復方案(例如微信的Tinker,qq空間的超級補丁)以及阿里系的基於Method Hook的熱修復方案(例如Andfix,Sophix等)。今天主要介紹的就是阿里巴巴的Andfix。git

二、源碼分析

如前所述,Andfix是阿里巴巴推出的一款基於Method Hook的熱修復技術,目前Github點贊數5.7K,是一款安全性高,較爲穩定,性能比較優異的方法級替換的熱修復技術。代碼實現上條理清晰,架構設計合理,可讀性強,是一個實現上很是優雅的開源框架。下面咱們重點介紹下Andfix的源碼及其設計。github

一個經典的開源框架首先要友好的對外暴露接口,這樣才能更便於接入,實現快速啓動。因此,在介紹核心源碼以前,咱們首先關注下Andfix的外部接口部分。 api

2.1. 初始化部分

爲了儘量的覆蓋BUG修復的範圍,和其餘的熱修復技術同樣,Andfix選擇在APP啓動的時候對熱補丁進行加載,也即Application的OnCreate過程。總體的外部接口調用以下所示:緩存

 1 @Override  2     public void onCreate() {  3         super.onCreate();  4         // patch的初始化
 5         mPatchManager = new PatchManager(this);  6         mPatchManager.init("1.0");  7         Log.d(TAG, "inited.");  8 
 9         // 加載緩存中的patch
10  mPatchManager.loadPatch(); 11         Log.d(TAG, "apatch loaded."); 12 
13         // 將外部存儲中的patch加載到當前運行的ART中
14         try { 15             // .apatch file path
16             String patchFileString = Environment.getExternalStorageDirectory() 17                     .getAbsolutePath() + APATCH_PATH; 18  mPatchManager.addPatch(patchFileString); 19             Log.d(TAG, "apatch:" + patchFileString + " added."); 20         } catch (IOException e) { 21             Log.e(TAG, "", e); 22  } 23     }

這部分接口很是簡潔,大概分爲三步:patch的初始化,patch的緩存加載,patch的外部存儲加載。 安全

緩存加載是爲了加載以前已經從外部存儲載入到緩存(data目錄下)中的patch,外部存儲加載是爲了從外部存儲中加載patch到緩存。Andfix的總體外部調用就是上面的幾步,下面咱們來看下Andfix的具體實現部分。服務器

2.2. 核心實現 

Andfix的具體實現上主要分爲三部分:Patch管理部分,Fix管理部分,Native Hook部分,其總體的UML架構圖以下所示: 微信

//todo 增長總體UML數據結構

 

 

2.2.1. PatchManager

總體的初始化函數的源碼以下:

 1   /**
 2  * Patch的初始化工做  3  * @param appVersion App的版本號  4      */
 5     public void init(String appVersion) {  6         if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
 7             Log.e(TAG, "patch dir create error.");  8             return;  9         } else if (!mPatchDir.isDirectory()) {// not directory
10  mPatchDir.delete(); 11             return; 12  } 13         SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, 14  Context.MODE_PRIVATE); 15         String ver = sp.getString(SP_VERSION, null); 16         if (ver == null || !ver.equalsIgnoreCase(appVersion)) { 17  cleanPatch(); 18  sp.edit().putString(SP_VERSION, appVersion).commit(); 19         } else { 20  initPatchs(); 21  } 22     }

其中mPatchDir表示data私有目錄下存放Patch文件的文件夾。首先是關於mPatchDir的簡單文件夾操做,在mPatchDir文件夾初始化完成以後,緊接着比較當前的APP版本和SharedPreferences中保存的Patch對應的APP版本,二者若是不相等的話,會直接清除掉本地緩存的Patch文件和對應Patch相關的數據。這是由於熱補丁是跟APP強相關的,Patch只能精確的修復對應版本的Bug。清除的源碼以下所示:

1     private void cleanPatch() { 2         File[] files = mPatchDir.listFiles(); 3         for (File file : files) { 4  mAndFixManager.removeOptFile(file); 5             if (!FileUtil.deleteFile(file)) { 6                 Log.e(TAG, file.getName() + " delete error."); 7  } 8  } 9     }

在版本號匹配以後,緊接着是Patch文件的初始化部分(initPatchs()),其源碼以下:

1     private void initPatchs() { 2         File[] files = mPatchDir.listFiles(); 3         for (File file : files) { 4  addPatch(file); 5  } 6     }

在上述函數中,ART會遍歷Patch文件,並將Patch文件經過addPatch方法添加到內存中。

addPatch方法有兩種多態實現,分別以下:

  • private Patch addPatch(File file)
  • public void addPatch(String path) throws IOException

其中第一個方法是從Patch文件中獲取Patch對象,具體的源碼以下:

 1 /**
 2  * add patch file  3  *  4  * @param file  5  * @return patch  6      */
 7     private Patch addPatch(File file) {  8         Patch patch = null;  9         if (file.getName().endsWith(SUFFIX)) { 10             try { 11                 patch = new Patch(file); 12  mPatchs.add(patch); 13             } catch (IOException e) { 14                 Log.e(TAG, "addPatch", e); 15  } 16  } 17         return patch; 18     }

此方法中把Patch文件夾映射爲Patch對象,而後將Patch對象統一存放在mPatchs數據集裏面。

第二個方法是從本地路徑中獲取Patch文件,而後從Patch文件中解析出Patch對象,以後觸發Patch的加載過程,具體源碼以下: 

 1 public void addPatch(String path) throws IOException {  2         File src = new File(path);  3         File dest = new File(mPatchDir, src.getName());  4         if(!src.exists()){  5             throw new FileNotFoundException(path);  6  }  7         if (dest.exists()) {  8             Log.d(TAG, "patch [" + path + "] has be loaded.");  9             return; 10  } 11         FileUtil.copyFile(src, dest);// copy to patch's directory
12         Patch patch = addPatch(dest); 13         if (patch != null) { 14  loadPatch(patch); 15  } 16     }

獲取完Patch的對象列表以後,接下來的內容就是加載Patch中的內容,並根據Patch中的內容進行Hotfix。此過程是經過Patchmanager類中的loadPatch方法實現的。loadPatch方法一共有三個多態,分別以下:

  • public void loadPatch(String patchName, ClassLoader classLoader)
  • public void loadPatch()
  • private void loadPatch(Patch patch)

三個方法入參不一樣,會經過不一樣的ClassLoader加載不一樣的Patch文件,已第三個方法爲例,該函數中對數據進行封裝以後,最終會循環調用AndfixManager中的fix方法,具體的源碼以下:

 1 private void loadPatch(Patch patch) {  2         Set<String> patchNames = patch.getPatchNames();  3  ClassLoader cl;  4         List<String> classes;  5         for (String patchName : patchNames) {  6             if (mLoaders.containsKey("*")) {  7                 cl = mContext.getClassLoader();  8             } else {  9                 cl = mLoaders.get(patchName); 10  } 11             if (cl != null) { 12                 classes = patch.getClasses(patchName); 13  mAndFixManager.fix(patch.getFile(), cl, classes); 14  } 15  } 16     }

PatchManager的源碼基本就如上所述,主要是對Patch的管理與加載過程,代碼簡潔易懂,可讀性強。

2.2.2. AndFixManager

接下來,咱們重點分析下AndfixManager類,該類中主要介紹Andfix的BugFix的核心流程。經過以前的PatchManager類的源碼分析可知,AndfixManager的關鍵入口函數爲fix方法。其源碼以下所示:

 1 public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {
 2         if (!mSupport) {
 3             return;
 4         }
 5 
 6         if (!mSecurityChecker.verifyApk(file)) {// security check fail
 7             return;
 8         }
 9 
10         try {
11             File optfile = new File(mOptDir, file.getName());
12             boolean saveFingerprint = true;
13             if (optfile.exists()) {
14                 // need to verify fingerprint when the optimize file exist,
15                 // prevent someone attack on jailbreak device with
16                 // Vulnerability-Parasyte.
17                 // btw:exaggerated android Vulnerability-Parasyte
18                 // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
19                 if (mSecurityChecker.verifyOpt(optfile)) {
20                     saveFingerprint = false;
21                 } else if (!optfile.delete()) {
22                     return;
23                 }
24             }
25 
26             final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
27                     optfile.getAbsolutePath(), Context.MODE_PRIVATE);
28 
29             if (saveFingerprint) {
30                 mSecurityChecker.saveOptSig(optfile);
31             }
32 
33             ClassLoader patchClassLoader = new ClassLoader(classLoader) {
34                 @Override
35                 protected Class<?> findClass(String className)
36                         throws ClassNotFoundException {
37                     Class<?> clazz = dexFile.loadClass(className, this);
38                     if (clazz == null
39                             && className.startsWith("com.alipay.euler.andfix")) {
40                         return Class.forName(className);// annotation’s class
41                                                         // not found
42                     }
43                     if (clazz == null) {
44                         throw new ClassNotFoundException(className);
45                     }
46                     return clazz;
47                 }
48             };
49             Enumeration<String> entrys = dexFile.entries();
50             Class<?> clazz = null;
51             while (entrys.hasMoreElements()) {
52                 String entry = entrys.nextElement();
53                 if (classes != null && !classes.contains(entry)) {
54                     continue;// skip, not need fix
55                 }
56                 clazz = dexFile.loadClass(entry, patchClassLoader);
57                 if (clazz != null) {
58                     fixClass(clazz, classLoader);
59                 }
60             }
61         } catch (IOException e) {
62             Log.e(TAG, "pacth", e);
63         }
64     }

在此方法中,主要包括安全校驗,bugFix兩部分,具體以下;

(1)安全校驗

Andfix會對傳進來的Patch文件進行安全校驗,包括準確性校驗和完整性校驗。

安全校驗的具體實如今SecurityChecker類中,其結構體以下:

//todo 補充SecurityChecker UML

其中準確性校驗(簽名校驗)的具體實現以下:

 1 /**
 2      * @param path
 3      *            Apk file
 4      * @return true if verify apk success
 5      */
 6     public boolean verifyApk(File path) {
 7         if (mDebuggable) {
 8             Log.d(TAG, "mDebuggable = true");
 9             return true;
10         }
11 
12         JarFile jarFile = null;
13         try {
14             jarFile = new JarFile(path);
15 
16             JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);
17             if (null == jarEntry) {// no code
18                 return false;
19             }
20             loadDigestes(jarFile, jarEntry);
21             Certificate[] certs = jarEntry.getCertificates();
22             if (certs == null) {
23                 return false;
24             }
25             return check(path, certs);
26         } catch (IOException e) {
27             Log.e(TAG, path.getAbsolutePath(), e);
28             return false;
29         } finally {
30             try {
31                 if (jarFile != null) {
32                     jarFile.close();
33                 }
34             } catch (IOException e) {
35                 Log.e(TAG, path.getAbsolutePath(), e);
36             }
37         }
38     }
39 
40     // verify the signature of the Apk
41     private boolean check(File path, Certificate[] certs) {
42         if (certs.length > 0) {
43             for (int i = certs.length - 1; i >= 0; i--) {
44                 try {
45                     certs[i].verify(mPublicKey);
46                     return true;
47                 } catch (Exception e) {
48                     Log.e(TAG, path.getAbsolutePath(), e);
49                 }
50             }
51         }
52         return false;
53     }

上述過程對APK進行證書籤名校驗,符合簽名的APK爲合法的APK,不然爲非法的APK,中斷熱修復過程。

Andfix的過程不只進行簽名校驗,還進行完整性校驗。完整性校驗是爲了防止出如今進行patch下載的過程當中下載不完整,致使修復出現異常的狀況。完整性校驗是經過校驗MD4來實現的,具體以下;

 1 /**
 2      * @param path
 3      *            Dex file
 4      * @return true if verify fingerprint success
 5      */
 6     public boolean verifyOpt(File file) {
 7         String fingerprint = getFileMD5(file);
 8         String saved = getFingerprint(file.getName());
 9         if (fingerprint != null && TextUtils.equals(fingerprint, saved)) {
10             return true;
11         }
12         return false;
13

 (2)Bug Fix

Andfix熱修復的核心實現中,分爲兩個步驟:

  1. 找到須要修復的Class;
  2. 替換須要進行修復的Method。

第一步的具體實現以下:

 1 /**
 2      * fix class
 3      * 
 4      * @param clazz
 5      *            class
 6      */
 7     private void fixClass(Class<?> clazz, ClassLoader classLoader) {
 8         Method[] methods = clazz.getDeclaredMethods();
 9         MethodReplace methodReplace;
10         String clz;
11         String meth;
12         for (Method method : methods) {
13             methodReplace = method.getAnnotation(MethodReplace.class);
14             if (methodReplace == null)
15                 continue;
16             clz = methodReplace.clazz();
17             meth = methodReplace.method();
18             if (!isEmpty(clz) && !isEmpty(meth)) {
19                 replaceMethod(classLoader, clz, meth, method);
20             }
21         }
22     }

第二部的具體實現以下:

 1 /**
 2      * replace method
 3      * 
 4      * @param classLoader classloader
 5      * @param clz class
 6      * @param meth name of target method 
 7      * @param method source method
 8      */
 9     private void replaceMethod(ClassLoader classLoader, String clz,
10             String meth, Method method) {
11         try {
12             String key = clz + "@" + classLoader.toString();
13             Class<?> clazz = mFixedClass.get(key);
14             if (clazz == null) {// class not load
15                 Class<?> clzz = classLoader.loadClass(clz);
16                 // initialize target class
17                 clazz = AndFix.initTargetClass(clzz);
18             }
19             if (clazz != null) {// initialize class OK
20                 mFixedClass.put(key, clazz);
21                 Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());
22                 AndFix.addReplaceMethod(src, method);
23             }
24         } catch (Exception e) {
25             Log.e(TAG, "replaceMethod", e);
26         }
27

其中核心函數AndFix.addReplaceMethod(src, method)的具體實現以下:

 1 /**
 2      * replace method's body
 3      * 
 4      * @param src
 5      *            source method
 6      * @param dest
 7      *            target method
 8      * 
 9      */
10     public static void addReplaceMethod(Method src, Method dest) {
11         try {
12             replaceMethod(src, dest);
13             initFields(dest.getDeclaringClass());
14         } catch (Throwable e) {
15             Log.e(TAG, "addReplaceMethod", e);
16         }
17

能夠觀察到,Andfix中函數的替換是經過Native方法replaceMethod(Method dest, Method src)實現的。從JNI中找到這部分的源碼以下:

1 static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) {
2     if (isArt) {
3         art_replaceMethod(env, src, dest);
4     } else {
5         dalvik_replaceMethod(env, src, dest);
6     }
7

Native層面進行Method Hook的原理是將源方法中的各個屬性替換爲目標方法的屬性。因爲不一樣虛擬機,甚至一樣虛擬機下不一樣API對應的方法結構體的不一樣,在進行Method Hook的過程當中,對不一樣狀況,要適配不一樣的方法。

不一樣的Android版本,對於的虛擬機不一樣:Android 4.4如下用的是Dalvik虛擬機,而Android 4.4以上用的是ART(Android Running Time)虛擬機。如上面代碼實現,在進行熱修復的過程當中,ART虛擬機下調用的是art_replaceMethod(env, src, dest)方法;Dalvik虛擬機調用的是dalvik_replaceMethod(env, src, dest)方法。 

而對於ART虛擬機,不一樣Android API的系統,可能會對應不一樣的方法結構體(ArtMethod),因此會有對應的不一樣的適配實現,其代碼以下:

 1 extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
 2         JNIEnv* env, jobject src, jobject dest) {
 3     if (apilevel > 23) {
 4         replace_7_0(env, src, dest);
 5     } else if (apilevel > 22) {
 6         replace_6_0(env, src, dest);
 7     } else if (apilevel > 21) {
 8         replace_5_1(env, src, dest);
 9     } else if (apilevel > 19) {
10         replace_5_0(env, src, dest);
11     }else{
12         replace_4_4(env, src, dest);
13     }
14 } 

不一樣API的實現類以下:

因此說Andfix能夠兼容Android2.3到7.0版本,對於超過Android7.0的版本,若是ArtMethod相比較7.0有較大的改變,就可能存在兼容性問題,這是後話。

以7.0版本爲例,Andfix中Method hook的屬性替換的具體實現以下:

 1 void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
 2     art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
 3 
 4     art::mirror::ArtMethod* dmeth =
 5             (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
 6 
 7 //    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ =
 8 //            reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader
 9     reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
10             reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
11     reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =
12             reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1;
13     //for reflection invoke
14     reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
15 
16     smeth->declaring_class_ = dmeth->declaring_class_;
17     smeth->access_flags_ = dmeth->access_flags_  | 0x0001;
18     smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
19     smeth->dex_method_index_ = dmeth->dex_method_index_;
20     smeth->method_index_ = dmeth->method_index_;
21     smeth->hotness_count_ = dmeth->hotness_count_;
22 
23     smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
24             dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
25     smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
26             dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
27 
28     smeth->ptr_sized_fields_.entry_point_from_jni_ =
29             dmeth->ptr_sized_fields_.entry_point_from_jni_;
30     smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
31             dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
32 
33     LOGD("replace_7_0: %d , %d",
34             smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
35             dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
36 
37

首先調用ART的方法獲取Andfix中源方法(smeth)和目標方法(dmeth)的句柄,而後將源方法的各個屬性(例如declaring_class_:所屬類,access_flags:訪問權限,method_index_:代碼執行地址等)替換爲目標方法的各個屬性,從而實現方法層面的Hook,實現Hotfix。

3.方案評價

3.1.優勢

(1)即時生效

(2)基於Method Hook的實現,對原始APK侵入較小,性能影響幾乎忽略不計

3.2.缺點

(1)只能用於方法級的修復

Andfix最爲明顯的缺點是隻能實現方法級別的修復。而沒法實現xml,資源文件級別的修復,也沒法增長或者刪除class類,這一點從原理分析上可以很明顯的看到。可是,熱修復的精髓就是在不從新發布版本,不影響性能和體驗的前提下,實現對線上緊急Bug的靈活修復。在大多數狀況下,經過方法級別的修復就可以達到熱修復的目的,Andfix作到了小而精,改動小,影響小但性能優異,效果穩定,我的認爲在必定程度上已經知足了熱修復的需求。與Andfix造成鮮明對比的是微信推出的Tinker,Tinker追求的是廣而博,可以實現類,xml,資源文件,so庫等的修復,甚至能夠新增export屬性爲false的Activity類,從某種意義上講,甚至能夠小型功能的發佈,有點插件化的味道。

這裏不過多評價兩種插件化框架的優劣,和談戀愛同樣,沒有最好的,只有最合適的,選擇適合本身項目的熱修復框架,而後用好,就能夠了。

(2)兼容性問題

因爲Java方法對應的底層數據結構體的差別,在進行native層面的Method Hook過程當中,不一樣虛擬機之間要使用不一樣的方法,甚至在ART架構中,不一樣的API的Android版本間也可能要使用不一樣的適配方法。

目前Andfix在實現的時候,根據AOSP開源代碼中不一樣API版本對ArtMethod的定義,將運行的Java Method強行地轉換爲art::mirror::ArtMethod,可是因爲Android源碼是公開的,在實際的設備上,不一樣的手機廠商可能會對ArtMethod作個性化修改,這樣就有可能會致使基於開源標準代碼實現的Method Hook沒法兼容有些設備的狀況。

爲了解決Andfix的兼容性問題,阿里巴巴隨後推出了Andfix的改進版熱修復方案——Sophix。Sophix與Andfix的區別在於,在進行Method Hook的時候,再也不進行ArtMethod屬性的替換,而是直接將ArtMethod做爲一個總體進行替換, 其Method Hook的核心實現以下:

  • memcpy(dmeth, smeth, sizeof(ArtMethod));

Sophix經過進行總體方法體的替換,完美的解決了Andfix中的兼容性問題,這樣,不只在不能的廠商的設備上能夠達到兼容,並且對於後續發佈的Android版本也可以作到向後兼容,保障了熱修復方案的健壯性。

4.總結與思考

本文對Andfix的原理進行了分析介紹,並對Andfix客戶端的源碼實現進行了簡要分析,其中重點介紹了客戶端在獲取Patch後進行Class匹配與Method替換的過程。

初次此外,在開發過程當中,有幾個技術細節也有較大的可挖掘性,具體以下:

(1)Andfix中熱修復Patch的生成原理;

(2)Patch的下載流程(推薦本身搭建服務器框架,經過okhttp實現下載流程),更新,版本管理;

(3)MultiDex下的Andfix;

(4)ClassLoader的內核原理;

(5)Android Running Time與Dalvik;

(6)其餘同類型的熱修復框架,例如騰訊微信的Tinker,美團的Robust,餓了麼的MiGo,大衆點評的Nuwa等。

5.參考文獻

相關文章
相關標籤/搜索