本文來自網易雲社區html
做者:王晨彥java
熱修復和插件化是目前 Android 領域很火熱的兩門技術,也是 Android 開發工程師必備的技能。android
目前比較流行的熱修復方案有微信的 Tinker,手淘的 Sophix,美團的 Robust,以及 QQ 空間熱修復方案。git
QQ 空間熱修復方案使用Java實現,比較容易上手。github
若是還不瞭解 QQ 空間方案的原理,請先學習安卓App熱補丁動態修復技術介紹api
今天,咱們就基於 QQ 空間方案來深刻學習熱修復原理,而且手把手完成一個熱修復框架。數組
本文參考了 Nuwa,在此表示感謝。微信
本文基於 Gradle 2.3.3 版本,支持 Gradle 1.5.0-3.0.1。app
瞭解了熱修復原理後,咱們就開始打造一個熱修復框架框架
關閉dex校驗
根據文章中提到的第一個問題,在 Android 5.0 以上,APK安裝時,爲了提升 dex 加載速度,未引用其餘 dex 的 class 將會被打上 CLASS_ISPREVERIFIED 標誌。
打上 CLASS_ISPREVERIFIED 標誌的 class,類加載器就不會去其餘 dex 中尋找 class,咱們就沒法使用插樁的方式替換 class。
文章給出瞭解決辦法,即讓全部類都依賴其餘 dex。如何實現呢?
新建一個 Hack 類,讓全部類都依賴該類,將該類打包成 dex,在應用啓動時優先將該 dex 插入到數組的最前面,便可實現。
OK,肯定思路後,咱們就開始動手。
找出編譯後的 class
聽起來好像很簡單,那麼如何讓全部類依賴 Hack 類呢,總不能一個一個類改吧,怎麼才能在打包時自動添加依賴呢?
接下來就要用到 Gradle Hook 和 ASM。
還不瞭解 Gradle 構建流程的趕快去學習啦
要想修改編譯後的 class 文件,首先要 Hook 打包過程,在 Gradle 編譯出 class 文件到打包成 APK 之間植入咱們的代碼,對 class 文件進行修改。
找到編譯後的class文件要依賴 Gradle Hook ,而修改 class 文件要依賴 ASM。
首先,咱們要找到編譯後的 class 文件
新建一個 Project CFixExample,而後執行 assembleDebug
觀察 Gradle Console 輸出
:app:preBuild UP-TO-DATE :app:preDebugBuild UP-TO-DATE :app:checkDebugManifest :app:preReleaseBuild UP-TO-DATE :app:prepareComAndroidSupportAnimatedVectorDrawable2540Library// 省略部分Task:app:prepareComAndroidSupportSupportVectorDrawable2540Library :app:prepareDebugDependencies :app:compileDebugAidl UP-TO-DATE :app:compileDebugRenderscript UP-TO-DATE :app:generateDebugBuildConfig UP-TO-DATE :app:generateDebugResValues UP-TO-DATE :app:generateDebugResources UP-TO-DATE :app:mergeDebugResources UP-TO-DATE :app:processDebugManifest UP-TO-DATE :app:processDebugResources UP-TO-DATE :app:generateDebugSources UP-TO-DATE :app:incrementalDebugJavaCompilationSafeguard :app:javaPreCompileDebug :app:compileDebugJavaWithJavac :app:compileDebugNdk NO-SOURCE :app:compileDebugSources :app:mergeDebugShaders :app:compileDebugShaders :app:generateDebugAssets :app:mergeDebugAssets :app:transformClassesWithDexForDebug :app:mergeDebugJniLibFolders :app:transformNativeLibsWithMergeJniLibsForDebug :app:processDebugJavaRes NO-SOURCE :app:transformResourcesWithMergeJavaResForDebug :app:validateSigningDebug :app:packageDebug :app:assembleDebug BUILD SUCCESSFUL in 10s
這些就是 Gradle 打包時執行的全部任務,不一樣版本的 Gradle 會有所不一樣,這裏咱們基於 Gradle 2.3.3。
請注意 processDebugManifest 和 transformClassesWithDexForDebug 這兩個Task,根據名字咱們能夠先猜想一下
第一個 Task 的做用應該是處理Manifest,這個咱們等會兒會用到
第二個 Task 的做用應該是將 class 轉換爲 dex,這不正是咱們要找的 Hook 點嗎?
沒錯,爲了驗證咱們的猜想,咱們打印一下 transformClassesWithDexForDebug 的輸入文件
在 app 的 build.gradle 中添加以下代碼
project.afterEvaluate { project.android.applicationVariants.each { variant -> Task transformClassesWithDexTask = project.tasks.findByName("transformClassesWithDexFor${variant.name.capitalize()}") println("transformClassesWithDexTask inputs") transformClassesWithDexTask.inputs.files.each { file -> println(file.absolutePath) } } }
再次打包,觀察輸出
transformClassesWithDexTask inputs C:\Users\hzwangchenyan\.android\build-cache\97c23f4056f5ee778ec4eb674107b6b52d506af5\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\6afe39630b2c3d3c77f8edc9b1e09a2c7198cd6d\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\c30268348acf4c4c07940f031070b72c4efa6bba\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\5b09d9d421b0a6929ae76b50c69f95b4a4a44566\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\e302262273df85f0776e06e63fde3eb1bdc3e57f\output\jars\classes.jar C:\Users\hzwangchenyan\.gradle\caches\modules-2\files-2.1\com.android.support\support-annotations\25.4.0\f6a2fc748ae3769633dea050563e1613e93c135e\support-annotations-25.4.0.jar C:\Users\hzwangchenyan\.android\build-cache\36b7224f035cc886381f4287c806a33369f1cb1a\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\5d757d92536f0399625abbab92c2127191e0d073\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\011eb26fd0abe9f08833171835fae10cfda5e045\output\jars\classes.jar D:\Android\sdk\extras\m2repository\com\android\support\constraint\constraint-layout-solver\1.0.2\constraint-layout-solver-1.0.2.jar C:\Users\hzwangchenyan\.android\build-cache\36b443908e839f37d7bd7eff1ea793f138f8d0dd\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\40634d621fa35fcca70280efe0ae897a9d82ef8f\output\jars\classes.jar D:\Android\AndroidStudioProjects\CFixExample\app\build\intermediates\classes\debug
build-cache 就是 support 包
看起來這些都是 app 依賴的 library,可是咱們本身的代碼呢
看看最後一行 app\build\intermediates\classes\debug 目錄
沒錯,正是咱們本身的代碼,看來咱們的猜想是正確的。
將 class 插入對 Hack 的引用[重點]
找到了編譯後的 class 文件,接下來使用 ASM 對 class 文件進行修改
ClassReader cr = new ClassReader(inputStream) ClassWriter cw = new ClassWriter(cr, 0) ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) { @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions) mv = new MethodVisitor(Opcodes.ASM4, mv) { @Override void visitInsn(int opcode) { if ("<init>".equals(name) && opcode == Opcodes.RETURN) { super.visitLdcInsn(Type.getType("Lme/wcy/cfix/Hack;")) } super.visitInsn(opcode) } } return mv } } cr.accept(cv, 0)
咱們經過複寫 ClassVisitor 的 visitMethod 方法,獲得 class 的全部方法,在構造函數中插入 Hack 類的引用。
能夠看到,即將打包爲dex的源文件既有 jar 又有 class,class 文件咱們直接修改就好,而對於 jar 文件,咱們須要先將其解壓,對解壓後的 class 文件進行修改,而後再壓縮。
File optDirFile = new File(jarFile.absolutePath.substring(0, jarFile.absolutePath.length() - 4)) File metaInfoDir = new File(optDirFile, "META-INF") File optJar = new File(jarFile.parent, jarFile.name + ".opt") CFixFileUtils.unZipJar(jarFile, optDirFile)if (metaInfoDir.exists()) { metaInfoDir.deleteDir() } optDirFile.eachFileRecurse { file -> if (file.isFile()) { processClass(file, hashFile, hashMap, patchDir, extension) } } CFixFileUtils.zipJar(optDirFile, optJar) jarFile.delete() optJar.renameTo(jarFile) optDirFile.deleteDir()
保存文件 Hash 值
咱們今天的目的是打造一個熱修復框架,因從咱們須要對於引入了 Hack 的 class 作一個記錄,讓咱們在修改代碼後打補丁包時能夠知道哪些類發生了改變,只須要打包修改了的類做爲補丁便可。
如何記錄呢,咱們知道,Java 在編譯時一樣的 Java 文件編譯爲 class 後字節碼是一致的,所以直接計算文件 Hash 值並保存便可。
製做補丁時對比 class 文件的 Hash 值,若是不一樣,則打包進補丁。
插入 Hack dex
新建 Hack.java
public class Hack { }
上面咱們提到,將包含 Hack 類的 dex 插入到 dex 數組的最前面,否則的話將會出現 Hack ClassNotFoundException,打包 dex 可使用 build tool 的 dx 命令,位於 /sdk/build-tools/version/dx
dx --dex --output=patch.jar classDir
打包爲 dex 並壓縮爲 jar
打包完成,如何插入到數組最前面呢,其實就和普通的補丁文件同樣,只不過在普通補丁以前插入
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader()); Object baseDexElements = getDexElements(getPathList(getPathClassLoader())); Object newDexElements = getDexElements(getPathList(dexClassLoader)); Object allDexElements = combineArray(newDexElements, baseDexElements); Object pathList = getPathList(getPathClassLoader()); ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements); }
這裏採用反射的方法,對 BaseDexClassLoader 的 dexElements 進行修改。
這個插入操做是在應用啓動時完成的,那 dex 文件從哪裏來呢,咱們能夠將 dex 放在 assets 中,插入前先將其複製到應用目錄。
這個操做咱們放在 Application 的 attachBaseContext 中執行。
網易雲免費體驗館,0成本體驗20+款雲產品!
更多網易研發、產品、運營經驗分享請訪問網易雲社區。
相關文章:
【推薦】 市場調研中數據分析之道
【推薦】 移動端爬蟲工具與方法介紹