寫這篇文章的目的也是爲了瞭解android源碼及hack技術,讀了這篇文章相信你也能夠了解到Dalvik的工做流程,apk的生成過程,及build.gradle中plugin中ApplicationPlugin的Task有哪些,如何經過hack技術來完成hotfix。有興趣的同窗也能夠看看groovy如何編寫Plugin,及如何優化dex來讓優化appjava
爲何會有熱修復這個東西呢?你們都知道若是咱們的線上的app 因爲某種緣由crash?咱們這時候不能怨測試沒測好,後臺接口有變化什麼的,這不是解決問題的最終方式!要是之前咱們確定就是把從新上傳app到各大渠道,重新上線,這個過程嚴重的影響到咱們的用戶體驗很是很差,並且很耗時!做爲程序員如何經過代碼進行線上修復crash bug。。。呢?因此有了熱修復這個功能 bat 每家都有本身的開源熱修復庫?我這裏就講一下如何經過反射的方式來實現修復功能吧!也就是經過DexClassLoader。若是你們對其餘的開源庫想要了解的話能夠經過一下傳送門
AndFix
tinker
HotFix
Robust
我這裏也就講一下Dex的方式修復android
咱們的項目如何在編譯的時候變成apk呢?git
這就是一個apk編譯所走的生命週期,可是咱們的build腳本到底走了哪些任務呢。若是想看的話能夠在咱們module中的build.gradle 加入以下代碼 便可在console中看到相應的任務程序員
tasks.whenTaskAdded { task ->
println(task.name+"===")
}複製代碼
這個就是咱們apkbuild的時候的每個task。既然知道了這些task 那咱們如何才能知道這些task到底在後臺作了些什麼呢?github
時常見到殊不知道他在幹嗎的一句代碼,apply plugin: 'com.android.application'若是咱們把com.android.application代替爲com.android.library,那咱們的build目錄下的output那就是aar包了。組件化開發會用到這樣的切換想了解的能夠看看(組件化)編程
想要了解這句話幹嗎的那你必須的知道這個開發語言groovy,他是支持android studio的。咱們能夠自定義咱們想要的插件,在編譯的時候進行一些好玩的操做。這裏面能夠定義許多task,迴歸正題,這句代碼到底幹了哪些事情呢,那咱們就必須的瞭解這個源碼想要了解的同窗能夠看看這裏就很少說了,這裏面有咱們build中的全部taskapi
####app啓動過程
以上了解了這麼多,接下來就要進入正題了!如今說app的啓動過程,過程就不細說了,由於經歷了不少複雜的過程,我就說一下與DexClassLoader有關的事情吧。數組
app每次啓動fork一個進程但同時也會一樣會分配一個dalvik虛擬機供這個app運行,在dalvik中每次運行都須要讀取apk裏面的dex文件,這樣會耗費不少cpu資源,而後採用odex,把dex文件優化成odex文件,那麼odex操做給咱們熱修復帶來了哪些問題呢?咱們先把這個問題記錄下來,以後會分析具體緣由。啓動先說到這!!!app
public Class findClass(String name, Listsuppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } 複製代碼
經過上面的步驟是否是知道了咱們app每次啓動一個class是如何找到類的呢?如今知道了吧,DexClassLoader -> DexPathList -> Element[]
好的 如今應該有一些系統的瞭解了,經過上面的步驟能夠知道 每次查找類都是經過Element[]中查找的。若是找到就會return 而不會繼續找!這時候嘿嘿嘿咱們知道了他是如何findclass 的那咱們就能夠悄悄的幹些壞事了(這裏會有一些同窗會懵逼,Element[]是什麼鬼)ide
咱們在每次建立DexClassLoader時他的構造函數是這樣的
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}複製代碼
從源碼能夠看出第一個參數顧名思義是dex路徑,第二個呢能夠看看源碼,第二個要傳一個路徑dex優化後odex的路徑。第三個呢就是父類嗎,直接getClassLoader()就好那麼咱們看看他的父類拿這些參數幹了些什麼
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}複製代碼
父類也就作了一些初始化操做。最主要的是初始化了DexPathList這個類,而後咱們看看BaseDexClassLoader裏面的findClass作了些什麼呢源碼以下
@Override
protected Class
findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}複製代碼
看到了嗎經過咱們構造函數初始化的DexPathList來查找的,上面咱們已經貼了DexPathList內部findclass的他是經過Element[]來拿到的。接下來咱們來看看DexPathList的構造函數
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}複製代碼
好了這就是他的源碼了可看到在夠着函數中有一個很重要的一步就是對(makeDexElements這個方法)Element[]初始化 說了這麼多終於到這個地方了 這是什麼鬼,進入這個方法來看一下
private static Element[] makeDexElements(ArrayListfiles, File optimizedDirectory) { ArrayList 複製代碼elements = new ArrayList (); /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { ZipFile zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE("Unable to load dex file: " + file, ex); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) { try { zip = new ZipFile(file); } catch (IOException ex) { /* * Note: ZipException (a subclass of IOException) * might get thrown by the ZipFile constructor * (e.g. if the file isn't actually a zip/jar * file). */ System.logE("Unable to open zip file: " + file, ex); } try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ignored) { /* * IOException might get thrown "legitimately" by * the DexFile constructor if the zip file turns * out to be resource-only (that is, no * classes.dex file in it). Safe to just ignore * the exception here, and let dex == null. */ } } else { System.logW("Unknown file type for: " + file); } if ((zip != null) || (dex != null)) { elements.add(new Element(file, zip, dex)); } } return elements.toArray(new Element[elements.size()]); }
代碼有點多哈...不要着急 我來慢慢說,首選呢構造函數傳進來了一個file數組不管是jar文件仍是apk文件咱們在這一步都是吧他們轉換成dex文件至關於作了一個操做把patch.jar 改爲patch.dex而後轉存到最初咱們傳進來的那個optimizedDirectory文件夾下。而後咱們的Element這個類是個靜態內部類,能夠看看下面的源碼的構造函數
public Element(File file, ZipFile zipFile, DexFile dexFile) {
this.file = file;
this.zipFile = zipFile;
this.dexFile = dexFile;
}複製代碼
能夠看到他傳入了這些參數。好了如今知道了這是什麼鬼了吧,一系列的源碼恐怕會看的頭暈腦脹的吧。反正知道了Element就是存儲咱們dex文件的每次findclass的時候從這裏面取得,知道這個就好了。
如今知道往哪裏插入咱們的dex文件了吧,只要在咱們app啓動的時候,把咱們的dex文件加載到Element[]數組最前面就好了,每次findclass的時候確定先查找咱們的dex了。這樣不就能夠達到熱修復了嗎!
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);複製代碼
這就是我所說的那四步。
大功告成是否是很帶勁吧。md終於把咱們的dex文件插入了。嘿嘿嘿黑科技啊,熱修復原來如此簡單。別高興的太早。接下來重點來了
這時候你運行項目的時候會發現app 掛了 哈哈哈 真是日了狗了,不出意外的話會報一下錯誤class ref in pre-verified class resolved to unexpected implementation 這個就是上面所說的odex操做帶來的麻煩。
出問題嗎?固然要慢慢解決了。先了解一下odex吧
這時候你們就要了解這個了javaassist,一個代碼植入庫,幾個簡單的api你們看看都會
/**
* 植入代碼
* @param buildDir 是項目的build class目錄,就是咱們須要注入的class所在地
* @param lib 這個是hackdex的目錄,就是AntilazyLoad類的class文件所在地
*/
public static void process(String buildDir, String lib) {
System.out.println(buildDir)
println(lib);
ClassPool classes = ClassPool.getDefault()
classes.appendClassPath(buildDir)
classes.appendClassPath(lib)
// 將須要關聯的類的構造方法中插入引用代碼
CtClass c = classes.getCtClass("cn.jiajixin.nuwasample.Hello.Hello")
if (c.isFrozen()) {
c.defrost()
}
println("====添加構造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(com.cuieney.hookdex.AntilazyLoad.class);")
c.writeFile(buildDir)
}複製代碼
若是咱們想不被打上標記就只能這樣了,就是經過這個方法,讓如今這個類Hello在當dex裏面引用其餘的dex文件裏面的AntilazyLoad.class簡單的說就是對其餘dex文件有依賴就不會被打上標記。
那麼咱們這個代碼段改在哪裏運行呢。好問題!!!這也是重點。不知道老鐵們還記得上面的代碼嗎。apk的生成過程的生命週期,就是在build的是那幾個步驟。咱們須要在.class文件編程成.dex文件前 進行代碼植入。這樣是否是很完美呢。那咱們從哪裏下手呢。固然是咱們的build.gradle文件下手,咱們編譯項目的時候每次是否是都是在這裏進行操做的
這裏要用到一個新的姿式哦(不對是知識哈哈哈)groovy這個語言plugin插件語言。咱們原生的android studio 是對groovy支持的。在咱們的項目中建立一個buildsrc項目,必定要這個名字。而後咱們在項目中建立一個類patch.groovy 目錄結構以下不用的都刪了。
而後咱們在咱們的app的build.gradle裏面作一下操做
task ('processWithJavassist') << {
String classPath = file('build/intermediates/classes/debug')//項目編譯class所在目錄
com.cuieney.groovy.PatchClass.process(classPath, project(':hookdex').buildDir
.absolutePath + '/intermediates/classes/debug')//第二個參數是hackdex的class所在目錄
}複製代碼
這個是執行代碼植入操做project(':hookdex')這個使咱們植入的類的module
可是我麼這個task你得保證在.class 到 .dex文件之間操做,咱們怎麼保證呢?接下來見證奇蹟的時候到了在build.g裏在添加以下代碼
applicationVariants.all { variant ->
variant.dex.dependsOn << processWithJavassist //在執行dx命令以前將代碼打入到class中
}複製代碼
這樣就完成了咱們的代碼植入操做 哈哈哈哈 牛逼不牛逼不
but 你在植入代碼以前必定要把咱們的植入的類的dex提早插入到Element[]裏面否則 會報找不到這個類的。 而後在只要真正的patch.dex 咱們的補丁。
包如下錯誤Gradle1.40 裏TransformAPI沒法打包的狀況,只兼容Gradle1.3-Gradle2.1.0版本
哈哈哈我也沒則,目前RocooFix這個項目博主經過一種新的方式進行了代碼植入(以前咱們經過植入代碼來完成避免打上標誌,他則是反其道而行,PatchClassLoader每次加載apk裏面的dex時,把標誌去了這樣也能夠防止出現以前那種crash 只能說牛逼牛逼,裏面代碼還在研究...)有興趣的同窗能夠看一下。
說了這麼多,其實網上這種帖子不少,本身只是想系統的整理一下,其實在這個過程本身學到了不少,不管是源碼仍是各方面的擴展知識吧,對本身都有很大的提高,不論老鐵們看沒看玩,但願此次分享給你們帶來的知識的提高。 stay hungry stay foolish
下一篇文章
手把手教你寫熱修復(HOTFIX)