接下來的幾篇博客我會用一個真實的demo來介紹如何實現(xiàn)熱修復。具體的內(nèi)容包括:
- 如何打包補丁包
- 如何將通過ClassLoader加載補丁包
1. 創(chuàng)建Demo
demo很簡單刹勃,創(chuàng)建一個只有一個Activity的demo:
package com.biyan.demo
public class MainActivity extends Activity {
private Calculator mCal;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCal = new Calculator();
}
public void click(View view) {
Toast.makeText(this, String.valueOf(mCal.calculate()),Toast.LENGTH_SHORT).show();
}
}
Public class Caculoator {
public float calculate() {
return 1 / 0;
}
}
demo的代碼很簡單让禀,運行會出什么bug也很清楚了,在此就不演示了簿透。
2.創(chuàng)建補丁包
首先修復Calculator的bug铅碍。
package com.biyan.demo
Public class Caculoator {
public float calculate() {
return 1 / 1;
}
}
重新編譯項目热鞍,在build目錄下找到Calculator.class文件,將其拷出來闷尿,準備打包塑径。放置在于Calculator包名相同的路徑下。
將其打成jar包:
jar -cvf patch.jar com
然后再將對應的jar包打成dex包:
dx --dex --output=patch_dex.jar patch.jar
dx是講jar包打成dex包的工具填具,安裝在path-android-sdk/build-tools/version(如24.0.0)/dx统舀。
patch_dex.jar就是補丁包,接下來將其安裝在sdCard中劳景,接下來應用從sdCard上加載該補丁包誉简。
3. 加載補丁
根據(jù)上一篇博客的介紹,加載補丁的思路如下:
- 在Application的onCreate()方法中獲取應用本身的
BaseDexClassLoader
,然后通過反射得到對應的dexElements - 創(chuàng)建一個新的DexClassLoader實例盟广,然后加載sdCard上的補丁包闷串,然后通過同樣的方法得到對應的dexElements
- 將兩個dexElements合并,然后再利用反射將合并后的dexElements賦值給應用本身的
BaseDexClassLoader
接下來看下具體代碼:
package com.hotpatch.demo;
import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
/**
* Created by hp on 2016/4/6.
*/
public class HotPatchApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 獲取補丁筋量,如果存在就執(zhí)行注入操作
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
File file = new File(dexPath);
if (file.exists()) {
inject(dexPath);
} else {
Log.e("BugFixApplication", dexPath + "不存在");
}
}
/**
* 要注入的dex的路徑
*
* @param path
*/
private void inject(String path) {
try {
// 獲取classes的dexElements
Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
Object pathList = getField(cl, "pathList", getClassLoader());
Object baseElements = getField(pathList.getClass(), "dexElements", pathList);
// 獲取patch_dex的dexElements(需要先加載dex)
String dexopt = getDir("dexopt", 0).getAbsolutePath();
DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
Object obj = getField(cl, "pathList", dexClassLoader);
Object dexElements = getField(obj.getClass(), "dexElements", obj);
// 合并兩個Elements
Object combineElements = combineArray(dexElements, baseElements);
// 將合并后的Element數(shù)組重新賦值給app的classLoader
setField(pathList.getClass(), "dexElements", pathList, combineElements);
//======== 以下是測試是否成功注入 =================
Object object = getField(pathList.getClass(), "dexElements", pathList);
int length = Array.getLength(object);
Log.e("BugFixApplication", "length = " + length);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/**
* 通過反射獲取對象的屬性值
*/
private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
/**
* 通過反射設置對象的屬性值
*/
private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
/**
* 通過反射合并兩個數(shù)組
*/
private Object combineArray(Object firstArr, Object secondArr) {
int firstLength = Array.getLength(firstArr);
int secondLength = Array.getLength(secondArr);
int length = firstLength + secondLength;
Class<?> componentType = firstArr.getClass().getComponentType();
Object newArr = Array.newInstance(componentType, length);
for (int i = 0; i < length; i++) {
if (i < firstLength) {
Array.set(newArr, i, Array.get(firstArr, i));
} else {
Array.set(newArr, i, Array.get(secondArr, i - firstLength));
}
}
return newArr;
}
}
核心代碼就這么多烹吵,接下來運行一下程序碉熄。程序還是Crash了。年叮。具被。
原因是類預校驗問題引起的:
- 在apk安裝的時候系統(tǒng)會將dex文件優(yōu)化成odex文件玻募,在優(yōu)化的過程中會涉及一個預校驗的過程
- 如果一個類的static方法只损,private方法,override方法以及構造函數(shù)中引用了其他類七咧,而且這些類都屬于同一個dex文件跃惫,此時該類就會被打上
CLASS_ISPREVERIFIED
- 如果在運行時被打上
CLASS_ISPREVERIFIED
的類引用了其他dex的類,就會報錯 - 所以
MainActivity
的onCreate()
方法中引用另一個dex的類就會出現(xiàn)上文中的問題 - 正常的分包方案會保證相關類被打入同一個dex文件
- 想要使得patch可以被正常加載艾栋,就必須保證類不會被打上
CLASS_ISPREVERIFIED
標記爆存。而要實現(xiàn)這個目的就必須要在分完包后的class中植入對其他dex文件中類的引用 - 要在已經(jīng)編譯完成后的類中植入對其他類的引用,就需要操作字節(jié)碼蝗砾,慣用的方案是插樁先较。常見的工具有javaassist,asm等悼粮。
其實QQ空間補丁方案的關鍵就在于字節(jié)碼的注入而不是dex的注入闲勺。下一篇博客將會介紹字節(jié)碼注入的相關細節(jié)。