Android熱修復學習之旅——Andfix框架完全解析
在之前的博客 《Android熱修復學習之旅——HotFix完全解析》 中,我們學習了熱修復的實現方式之一,通過dex分包方案的原理還有HotFix框架的源碼分析,本次我將講解熱修復的另外一種思路,那就是通過native方法,使用這種思路的框架代表就是阿里的Andfix,本篇博客,我們將深入分析Andfix的實現。
Andfix的使用
下面一段代碼就是Andfix的使用代碼,為了方便大家理解,重要內容已進行注釋
public class MainApplication extends Application { private static final String TAG = "euler"; private static final String APATCH_PATH = "/out.apatch";//被修復的文件都是以.apatch結尾 /** * patch manager */ private PatchManager mPatchManager; @Override public void onCreate() { super.onCreate(); // initialize //初始化PatchManager,也就是修復包的管理器,因為修復包可能有多個,所以這里需要一個管理器進行管理 mPatchManager = new PatchManager(this); mPatchManager.init("1.0"); Log.d(TAG, "inited."); // load patch //開始加載修復包 mPatchManager.loadPatch(); Log.d(TAG, "apatch loaded."); // add patch at runtime try { // .apatch file path //存放patch補丁文件的路徑,這里使用的sd卡,真實項目中肯定是從服務器下載到sd卡中 String patchFileString = Environment.getExternalStorageDirectory() .getAbsolutePath() + APATCH_PATH; mPatchManager.addPatch(patchFileString); Log.d(TAG, "apatch:" + patchFileString + " added."); } catch (IOException e) { Log.e(TAG, "", e); } } }
其實就是通過一個PatchManager加載修復包,接下來我們分析一下PatchManager的代碼
/** * @param context * context */ public PatchManager(Context context) { mContext = context; //初始化AndFixManager mAndFixManager = new AndFixManager(mContext); //初始化存放patch補丁文件的目錄 mPatchDir = new File(mContext.getFilesDir(), DIR); //初始化存在Patch類的集合 mPatchs = new ConcurrentSkipListSet<Patch>(); //初始化存放類對應的類加載器集合 mLoaders = new ConcurrentHashMap<String, ClassLoader>(); }
里面很重要的類就是AndFixManager,接下來我們看一下AndFixManager的初始化代碼
public AndFixManager(Context context) { mContext = context; //判斷Android機型是否適支持AndFix mSupport = Compat.isSupport(); if (mSupport) { //初始化簽名安全判斷類,此類主要是進行修復包安全校驗的工作 mSecurityChecker = new SecurityChecker(mContext); //初始化patch文件存放的目錄 mOptDir = new File(mContext.getFilesDir(), DIR); if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail mSupport = false; Log.e(TAG, "opt dir create error."); } else if (!mOptDir.isDirectory()) {// not directory //如果不是文件目錄就刪除 mOptDir.delete(); mSupport = false; } }
概括一下AndFixManager的初始化,主要做了以下的工作:
1.判斷Android機型是否適支持AndFix,
2.初始化修復包安全校驗的工作
Andfix源碼分析
首先看一下isSupport方法內部的邏輯
public static synchronized boolean isSupport() { if (isChecked) return isSupport; isChecked = true; // not support alibaba's YunOs if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) { isSupport = true; } if (inBlackList()) { isSupport = false; } return isSupport; }
可以看到判斷的條件主要是3個:
1.判斷系統是否是YunOs系統
@SuppressLint("DefaultLocale") private static boolean isYunOS() { String version = null; String vmName = null; try { Method m = Class.forName("android.os.SystemProperties").getMethod( "get", String.class); version = (String) m.invoke(null, "ro.yunos.version"); vmName = (String) m.invoke(null, "java.vm.name"); } catch (Exception e) { // nothing todo } if ((vmName != null && vmName.toLowerCase().contains("lemur")) || (version != null && version.trim().length() > 0)) { return true; } else { return false; } }
2.判斷是Dalvik還是Art虛擬機,來注冊Native方法
/** * initialize * * @return true if initialize success */ public static boolean setup() { try { final String vmVersion = System.getProperty("java.vm.version"); boolean isArt = vmVersion != null && vmVersion.startsWith("2"); int apilevel = Build.VERSION.SDK_INT; return setup(isArt, apilevel); } catch (Exception e) { Log.e(TAG, "setup", e); return false; } }
如果版本符合的話,會調用native的setup
static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart, jint apilevel) { isArt = isart; LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"), (int )apilevel); if (isArt) { return art_setup(env, (int) apilevel); } else { return dalvik_setup(env, (int) apilevel); } }
同樣在jboolean setup中分為art_setup和dalvik_setup
art_setup方法
extern jboolean __attribute__ ((visibility ("hidden"))) art_setup(JNIEnv* env, int level) { apilevel = level; return JNI_TRUE; }
dalvik_setup方法
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup( JNIEnv* env, int apilevel) { //打開系統的"libdvm.so"文件 void* dvm_hand = dlopen("libdvm.so", RTLD_NOW); if (dvm_hand) { //獲取dvmDecodeIndirectRef_fnPtr和dvmThreadSelf_fnPtr倆個函數 //這兩個函數可以通過類對象獲取ClassObject結構體 dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand, apilevel > 10 ? "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" : "dvmDecodeIndirectRef"); if (!dvmDecodeIndirectRef_fnPtr) { return JNI_FALSE; } dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand, apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf"); if (!dvmThreadSelf_fnPtr) { return JNI_FALSE; } //通過Java層Method對象的getDeclaringClass方法 //后續會調用該方法獲取某個方法所屬的類對象 //因為Java層只傳遞了Method對象到native層 jclass clazz = env->FindClass("java/lang/reflect/Method"); jClassMethod = env->GetMethodID(clazz, "getDeclaringClass", "()Ljava/lang/Class;"); return JNI_TRUE; } else { return JNI_FALSE; } }
主要做了兩件事,準備后續的replaceMethod函數中使用:
1、在libdvm.so動態獲取dvmDecodeIndirectRef_fnPtr函數指針和獲取dvmThreadSelf_fnPtr函數指針。
2、調用dest的 Method.getDeclaringClass方法獲取method的類對象clazz。
3.根據sdk版本判斷是否支持(支持Android2.3-7.0系統版本)
// from android 2.3 to android 7.0 private static boolean isSupportSDKVersion() { if (android.os.Build.VERSION.SDK_INT >= 8 && android.os.Build.VERSION.SDK_INT <= 24) { return true; } return false; }
然后我們看一下初始化簽名安全判斷類的代碼
public SecurityChecker(Context context) { mContext = context; init(mContext); }
init方法要是獲取當前應用的簽名及其他信息,為了判斷與patch文件的簽名是否一致
// initialize,and check debuggable //主要是獲取當前應用的簽名及其他信息,為了判斷與patch文件的簽名是否一致 private void init(Context context) { try { PackageManager pm = context.getPackageManager(); String packageName = context.getPackageName(); PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); CertificateFactory certFactory = CertificateFactory .getInstance("X.509"); ByteArrayInputStream stream = new ByteArrayInputStream( packageInfo.signatures[0].toByteArray()); X509Certificate cert = (X509Certificate) certFactory .generateCertificate(stream); mDebuggable = cert.getSubjectX500Principal().equals(DEBUG_DN); mPublicKey = cert.getPublicKey(); } catch (NameNotFoundException e) { Log.e(TAG, "init", e); } catch (CertificateException e) { Log.e(TAG, "init", e); } }
接下來是分析mPatchManager.init方法
public void init(String appVersion) { if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail Log.e(TAG, "patch dir create error."); return; } else if (!mPatchDir.isDirectory()) {// not directory mPatchDir.delete(); return; } //使用SP存儲關于patch文件的信息 SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); //根據你傳入的版本號和之前的對比,做不同的處理 String ver = sp.getString(SP_VERSION, null); if (ver == null || !ver.equalsIgnoreCase(appVersion)) { //刪除本地patch文件 cleanPatch(); //并把傳入的版本號保存 sp.edit().putString(SP_VERSION, appVersion).commit(); } else { //初始化patch列表,把本地的patch文件加載到內存 initPatchs(); } }
主要是進行版本號的對比,如果不一致則刪除本地所有的patch文件,同時保存新的版本號,否則就直接把本地的patch文件加載到內存
private void cleanPatch() { File[] files = mPatchDir.listFiles(); for (File file : files) { //刪除所有的本地緩存patch文件 mAndFixManager.removeOptFile(file); if (!FileUtil.deleteFile(file)) { Log.e(TAG, file.getName() + " delete error."); } } }
private void initPatchs() { File[] files = mPatchDir.listFiles(); for (File file : files) { addPatch(file); } }
/** * add patch file * * @param file * @return patch */ private Patch addPatch(File file) { Patch patch = null; if (file.getName().endsWith(SUFFIX)) { try { //創建Patch對象 patch = new Patch(file); //把patch實例存儲到內存的集合中,在PatchManager實例化集合 mPatchs.add(patch); } catch (IOException e) { Log.e(TAG, "addPatch", e); } } return patch; }
Patch類無疑是進行修復的關鍵,所以我們需要查看Patch的代碼
public Patch(File file) throws IOException { mFile = file; init(); }
@SuppressWarnings("deprecation") private void init() throws IOException { JarFile jarFile = null; InputStream inputStream = null; try { //使用JarFile讀取Patch文件 jarFile = new JarFile(mFile); //獲取META-INF/PATCH.MF文件 JarEntry entry = jarFile.getJarEntry(ENTRY_NAME); inputStream = jarFile.getInputStream(entry); Manifest manifest = new Manifest(inputStream); Attributes main = manifest.getMainAttributes(); //獲取PATCH.MF文件中的屬性Patch-Name mName = main.getValue(PATCH_NAME); //獲取PATCH.MF屬性Created-Time mTime = new Date(main.getValue(CREATED_TIME)); mClassesMap = new HashMap<String, List<String>>(); Attributes.Name attrName; String name; List<String> strings; for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) { attrName = (Attributes.Name) it.next(); name = attrName.toString(); //判斷name的后綴是否是-Classes,并把name對應的值加入到集合中,對應的值就是class類名的列表 if (name.endsWith(CLASSES)) { strings = Arrays.asList(main.getValue(attrName).split(",")); if (name.equalsIgnoreCase(PATCH_CLASSES)) { mClassesMap.put(mName, strings); } else { mClassesMap.put( //為了移除掉"-Classes"的后綴 name.trim().substring(0, name.length() - 8),// remove // "-Classes" strings); } } } } finally { if (jarFile != null) { jarFile.close(); } if (inputStream != null) { inputStream.close(); } } }
init方法主要的邏輯就是通過讀取.patch文件,每個修復包apatch文件其實都是一個jarFile文件,然后獲得其中META-INF/PATCH.MF文件,PATCH.MF文件中都是key-value的形式,獲取key是-Classes的所有的value,這些value就是所有要修復的類,他們是以“,”進行分割的,將它們放入list列表,將其存儲到一個集合中mClassesMap,list列表中存儲的就是所有要修復的類名
還有另一個addpath方法,接受的是文件路徑參數:
/** * add patch at runtime * * @param path * patch path * @throws IOException */ public void addPatch(String path) throws IOException { File src = new File(path); File dest = new File(mPatchDir, src.getName()); if(!src.exists()){ throw new FileNotFoundException(path); } if (dest.exists()) { Log.d(TAG, "patch [" + path + "] has be loaded."); return; } //把文件拷貝到專門存放patch文件的文件夾中 FileUtil.copyFile(src, dest);// copy to patch's directory Patch patch = addPatch(dest); if (patch != null) { //使用loadPatch進行加載 loadPatch(patch); } }
總結一下兩個addPatch方法的不同之處:
addPatch(file)方法:需要結合上面的initPatchs方法一起使用,他調用的場景是:本地mPatchDir目錄中已經有了修復包文件,并且版本號沒有發生變化,這樣每次啟動程序的時候就會調用初始化操作,在這里會遍歷mPatchDir目錄中所有的修復包文件,然后調用這個方法添加到全局文件列表中,也即是mPatchs中。
addPatch(String path)方法:這個方法使用的場景是版本號發生變化,或者是本地目錄中沒有修復包文件。比如第一次操作的時候,會從網絡上下載修復包文件,下載成功之后會把這個文件路徑通過這個方法調用即可,執行完之后也會主動調用加載修復包的操作了,比如demo中第一次在SD卡中放了一個修復包文件:
// add patch at runtime try { // .apatch file path //存放patch補丁文件的路徑,這里使用的sd卡,真實項目中肯定是從服務器下載到sd卡中 String patchFileString = Environment.getExternalStorageDirectory() .getAbsolutePath() + APATCH_PATH; mPatchManager.addPatch(patchFileString); Log.d(TAG, "apatch:" + patchFileString + " added."); } catch (IOException e) { Log.e(TAG, "", e); }
接下來,看一下mPatchManager.loadPatch();
/** * load patch,call when application start * */ public void loadPatch() { mLoaders.put("*", mContext.getClassLoader());// wildcard Set<String> patchNames; List<String> classes; for (Patch patch : mPatchs) { patchNames = patch.getPatchNames(); for (String patchName : patchNames) { //獲取patch對應的class類的集合List classes = patch.getClasses(patchName); //調用mAndFixManager.fix修復bug mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(), classes); } } }
這個方法主要是通過Patch類獲取修復包所有的修復類名稱,之前已經介紹了Patch類的初始化操作,在哪里會解析修復包的MF文件信息,獲取到修復包需要修復的類名然后保存到列表中,這里就通過getClasses方法來獲取指定修復包名稱對應的修復類名稱列表,然后調用AndFixManager的fix方法
接下來就是分析mAndFixManager.fix方法
/** * fix * * @param patchPath * patch path */ public synchronized void fix(String patchPath) { fix(new File(patchPath), mContext.getClassLoader(), null); }
** * fix * * @param file * patch file * @param classLoader * classloader of class that will be fixed * @param classes * classes will be fixed */ public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) { if (!mSupport) { return; } //判斷patch文件的簽名,檢查修復包的安全性 if (!mSecurityChecker.verifyApk(file)) {// security check fail return; } try { File optfile = new File(mOptDir, file.getName()); boolean saveFingerprint = true; if (optfile.exists()) { // need to verify fingerprint when the optimize file exist, // prevent someone attack on jailbreak device with // Vulnerability-Parasyte. // btw:exaggerated android Vulnerability-Parasyte // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html if (mSecurityChecker.verifyOpt(optfile)) { saveFingerprint = false; } else if (!optfile.delete()) { return; } } //使用dexFile 加載修復包文件,所以說patch文件其實本質是dex文件 final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), optfile.getAbsolutePath(), Context.MODE_PRIVATE); if (saveFingerprint) { mSecurityChecker.saveOptSig(optfile); } //這里重新new了一個ClasLoader,并重寫findClass方法 ClassLoader patchClassLoader = new ClassLoader(classLoader) { @Override protected Class<?> findClass(String className) throws ClassNotFoundException { Class<?> clazz = dexFile.loadClass(className, this); if (clazz == null && className.startsWith("com.alipay.euler.andfix")) { return Class.forName(className);// annotation’s class // not found } if (clazz == null) { throw new ClassNotFoundException(className); } return clazz; } }; Enumeration<String> entrys = dexFile.entries(); Class<?> clazz = null; while (entrys.hasMoreElements()) { String entry = entrys.nextElement(); if (classes != null && !classes.contains(entry)) { continue;// skip, not need fix } //加載有bug的類文件 clazz = dexFile.loadClass(entry, patchClassLoader); if (clazz != null) { //fixClass方法對有bug的文件進行替換 fixClass(clazz, classLoader); } } } catch (IOException e) { Log.e(TAG, "pacth", e); } }
概括一下fix方法做的幾件事:
1.使用mSecurityChecker進行修復包的校驗工作,這里的校驗就是比對修復包的簽名和應用的簽名是否一致:
/** * @param path * Apk file * @return true if verify apk success */ public boolean verifyApk(File path) { if (mDebuggable) { Log.d(TAG, "mDebuggable = true"); return true; } JarFile jarFile = null; try { jarFile = new JarFile(path); JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX); if (null == jarEntry) {// no code return false; } loadDigestes(jarFile, jarEntry); Certificate[] certs = jarEntry.getCertificates(); if (certs == null) { return false; } return check(path, certs); } catch (IOException e) { Log.e(TAG, path.getAbsolutePath(), e); return false; } finally { try { if (jarFile != null) { jarFile.close(); } } catch (IOException e) { Log.e(TAG, path.getAbsolutePath(), e); } } }
2.使用DexFile和自定義類加載器來加載修復包文件
//這里重新new了一個ClasLoader,并重寫findClass方法 ClassLoader patchClassLoader = new ClassLoader(classLoader) { @Override protected Class<?> findClass(String className) throws ClassNotFoundException { Class<?> clazz = dexFile.loadClass(className, this); if (clazz == null && className.startsWith("com.alipay.euler.andfix")) { return Class.forName(className);// annotation’s class // not found } if (clazz == null) { throw new ClassNotFoundException(className); } return clazz; } }; Enumeration<String> entrys = dexFile.entries(); Class<?> clazz = null; while (entrys.hasMoreElements()) { String entry = entrys.nextElement(); if (classes != null && !classes.contains(entry)) { continue;// skip, not need fix } //加載修復包patch中的文件信息,獲取其中要修復的類名,然后進行加載 clazz = dexFile.loadClass(entry, patchClassLoader); if (clazz != null) { //fixClass方法對有bug的文件進行替換 fixClass(clazz, classLoader); } }
這里創建一個新的classLoader的原因是,我們需要獲取修復類中bug的方法名稱,而這個方法名稱是通過修復方法的注解來獲取到的,所以得先進行類的加載然后獲取到他的方法信息,最后通過分析注解獲取方法名,這里用的是反射機制來進行操作的。使用自定義的classLoader為了過濾我們需要加載的類
接下來是fixClass方法的邏輯
/** * fix class * * @param clazz * class */ private void fixClass(Class<?> clazz, ClassLoader classLoader) { Method[] methods = clazz.getDeclaredMethods(); MethodReplace methodReplace; String clz; String meth; for (Method method : methods) { //遍歷所有的方法,獲取方法的注解,因為有bug的方法在生成的patch的類中的方法都是有注解的 methodReplace = method.getAnnotation(MethodReplace.class); if (methodReplace == null) continue; //獲取注解中clazz的值 clz = methodReplace.clazz(); //獲取注解中method的值 meth = methodReplace.method(); if (!isEmpty(clz) && !isEmpty(meth)) { //進行替換 replaceMethod(classLoader, clz, meth, method); } } }
通過反射獲取指定類名需要修復類中的所有方法類型,然后在獲取對應的注解信息,上面已經分析了通過DexFile加載修復包文件,然后在加載上面Patch類中的getClasses方法獲取到的修復類名稱列表來進行類的加載,然后在用反射機制獲取類中所有的方法對應的注解信息,通過注解信息獲取指定修復的方法名稱,看一下注解的定義:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MethodReplace { String clazz(); String method(); }
兩個方法:一個是獲取當前類名稱,一個是獲取當前方法名稱
/** * replace method * * @param classLoader classloader * @param clz class * @param meth name of target method * @param method source method */ private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) { try { String key = clz + "@" + classLoader.toString(); //判斷此類是否已經被fix Class<?> clazz = mFixedClass.get(key); if (clazz == null) {// class not load Class<?> clzz = classLoader.loadClass(clz); // initialize target class clazz = AndFix.initTargetClass(clzz);//初始化class } if (clazz != null) {// initialize class OK mFixedClass.put(key, clazz); //根據反射獲取到有bug的類的方法(有bug的apk) Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes()); //src是有bug的方法,method是補丁方法 AndFix.addReplaceMethod(src, method); } } catch (Exception e) { Log.e(TAG, "replaceMethod", e); } }
這里說明一下,獲得有bug方法的這段代碼:
Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());
通過方法名和本地已有的該方法的參數信息獲取有bug的方法,然后將有bug的方法和修復的方法一起傳入進行修復
注意:上面的操作,傳入的是修復新的方法信息以及需要修復的舊方法名稱,不過這里得先獲取到舊方法類型,可以看到修復的新舊方法的簽名必須一致,所謂簽名就是方法的名稱,參數個數,參數類型都必須一致,不然這里就報錯的。進而也修復不了了。
接下來就是交給native方法了,由于Android4.4后才用的Art虛擬機,之前的系統都是Dalvik虛擬機,因此Natice層寫了2個方法,對不同的系統做不同的處理方式。
#andfix.cpp static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) { if (isArt) { art_replaceMethod(env, src, dest); } else { dalvik_replaceMethod(env, src, dest); } }
Dalvik replaceMethod的實現:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod( JNIEnv* env, jobject src, jobject dest) { jobject clazz = env->CallObjectMethod(dest, jClassMethod); //ClassObject結構體包含很多信息,在native中這個值很有用 ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr( dvmThreadSelf_fnPtr(), clazz); clz->status = CLASS_INITIALIZED;//更改狀態為類初始化完成的狀態 //通過java層傳遞的方法對象,在native層獲得他們的結構體 Method* meth = (Method*) env->FromReflectedMethod(src); Method* target = (Method*) env->FromReflectedMethod(dest); LOGD("dalvikMethod: %s", meth->name); // meth->clazz = target->clazz; //核心方法如下,就是替換新舊方法結構體中的信息 meth->accessFlags |= ACC_PUBLIC; meth->methodIndex = target->methodIndex; meth->jniArgInfo = target->jniArgInfo; meth->registersSize = target->registersSize; meth->outsSize = target->outsSize; meth->insSize = target->insSize; meth->prototype = target->prototype; meth->insns = target->insns; meth->nativeFunc = target->nativeFunc; }
簡單來說,就是通過上層傳遞過來的新舊方法類型對象,通過JNIEnv的FromReflectedMethod方法獲取對應的方法結構體信息,然后將其信息進行替換即可
其余art的native方法,讀者可以自行閱讀,因為原理也是差不多.
如何生成patch包
細心的同學發現,我們還沒說如何生成patch包,可以通過apatch進行生成
使用神器apatch進行線上發布的release包和這次修復的fix包進行比對,獲取到修復文件apatch
java -jar apkpatch.jar -f app-release-fix.apk -t app-release-online.apk -o C:\Users\mayu-g\Desktop\apkpatch-1.0.3 -k myl.keystore -p 123456 -a mayunlong -e 123456
使用命令的時候需要用到簽名文件,因為在前面分析代碼的時候知道會做修復包的簽名驗證。這里得到了一個修復包文件如下:
而且會產生一個diff.dex文件和smali文件夾,而我們用壓縮軟件可以打開apatch文件看看:
可以看到這里的classes.dex文件其實就是上面的diff.dex文件,只是這里更像是Android中的apk文件目錄格式,同樣有一個META-INF目錄,這里存放了簽名文件以及需要修復類信息的PATCH.MF文件:
至此,Andfix框架已基本分析完畢。
來自:http://blog.csdn.net/u012124438/article/details/64623253