轉載請標註來源:http://www.cnblogs.com/charles04/p/8471301.htmlhtml
熱修復技術是移動端領域近年很是活躍的一項新技術,經過熱修復技術能夠在不發佈應用市場版本,在用戶無感知的狀況下對線上Bug進行緊急修復。正所謂修復於千里以外,剿滅與無形之中,實乃移動端開發運營中一項必備之尖端技術。其主要的運行原理以下: android
簡而言之,熱修復就是經過必定的技術手段,讓用戶在程序的實際運行操做中,走到修復的Patch邏輯序列,而繞開存在問題的邏輯片斷,實現問題的緊急規避。目前實現的技術手段主要有騰訊系的基於ClassLoader的熱修復方案(例如微信的Tinker,qq空間的超級補丁)以及阿里系的基於Method Hook的熱修復方案(例如Andfix,Sophix等)。今天主要介紹的就是阿里巴巴的Andfix。git
如前所述,Andfix是阿里巴巴推出的一款基於Method Hook的熱修復技術,目前Github點贊數5.7K,是一款安全性高,較爲穩定,性能比較優異的方法級替換的熱修復技術。代碼實現上條理清晰,架構設計合理,可讀性強,是一個實現上很是優雅的開源框架。下面咱們重點介紹下Andfix的源碼及其設計。github
一個經典的開源框架首先要友好的對外暴露接口,這樣才能更便於接入,實現快速啓動。因此,在介紹核心源碼以前,咱們首先關注下Andfix的外部接口部分。 api
爲了儘量的覆蓋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的具體實現部分。服務器
Andfix的具體實現上主要分爲三部分:Patch管理部分,Fix管理部分,Native Hook部分,其總體的UML架構圖以下所示: 微信
//todo 增長總體UML數據結構
總體的初始化函數的源碼以下:
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方法有兩種多態實現,分別以下:
其中第一個方法是從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方法一共有三個多態,分別以下:
三個方法入參不一樣,會經過不一樣的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的管理與加載過程,代碼簡潔易懂,可讀性強。
接下來,咱們重點分析下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 /** 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。
(2)基於Method Hook的實現,對原始APK侵入較小,性能影響幾乎忽略不計
(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的核心實現以下:
Sophix經過進行總體方法體的替換,完美的解決了Andfix中的兼容性問題,這樣,不只在不能的廠商的設備上能夠達到兼容,並且對於後續發佈的Android版本也可以作到向後兼容,保障了熱修復方案的健壯性。
本文對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等。