Android應用熱修復
一、修復的工具
當前主要有兩個主流的熱修復工具:
1.阿里系:使用了DeXposed(修改了國外的),一年沒有維護了,現在又搞了一個andfix,是一種黑客技術。自己去實現了底層的zyqote。從底層C的二進位制來入手的。
2.騰訊系:tinker
Java類載入機制來入手的。
這裡我們使用tinker。
二、熱修復的原理(Java類載入機制)
什麼是熱修復?
一般的Bug修復,都是等下一個版本解決,然後釋出新的apk
熱修復:可以直接在客戶已經安裝的程式當中修復bug。
bug一般會出現在某個類的某個方法地方。如果我們能夠動態的將客戶手機裡面的apk裡面的某個類給替換成我們已經修復好的類。
AndroidStudio的Instant run,也是一種熱修復或者增量更新的方式。如果只是改了一個類,那麼它只會把修改的東西打進新的包裡,放到手機上執行。
所以,在用AndroidStudio做熱修復的時候記得把Instant run功能關閉。不然會影響熱修復實現。
機制:dex分包。mutildex。
如何實現呢?實現的原理?
從Java的類載入機制來入手的:ClassLoader
Android 是如何載入class.dex檔案,啟動程式。
這裡提供了兩個類:
1.PathClassLoader :這個類用來載入應用程式的dex
public class PathClassLoader extends BaseDexClassLoader {
}
2.DexClassLoader :這個類可以載入指定的某個dex檔案。(限制:必須要在應用程式的目錄下面)
public class DexClassLoader extends BaseDexClassLoader {
}
修復方案:
1.搞多個dex
第一個版本:classes.dex。
修復後的補丁包:classes2.dex(包涵了我們修復xxx.class)
這種實現方式也可以用於外掛開發。
2.把兩個dex合併
將修復的class替換原來出bug的class.
通過BaseDexClassLoader呼叫findClass(className):
Class<?> findClass(String name)
實際上替換的是修復了的dex檔案,這裡面集成了修改了的class檔案。Element[] dexElements;儲存的是dex的集合。
在findClass方法中是通過迴圈去找dexElements中的類,如果找到了,就會返回這個Class,停止執行尋找。
所以可以採取以下方式:
將修復好的dex插入到dexElements的集合,位置:出現bug的xxx.class所在的dex的前面。
最本質的實現原理:類載入器去載入某個類的時候,是去dexElements裡面從頭往下查詢的。
fixed.dex,classes1.dex,classes2.dex,classes3.dex
三、如何實現
上面已經介紹了其中的原理,接下來在開發中如何具體實現。
步驟
1.先安裝一個帶有bug的版本apk。
2.修復bug,重新打包成dex檔案,放在後臺伺服器。
3.通過主動方式或者推送將修復的dex檔案,放到手機中,進行dex檔案的合併。
用AndroidStudio打包multidex(官方待驗證)
準備工作
1.配置gradle檔案:
1)
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
2)
defaultConfig {
multiDexEnabled true
}
3)
buildTypes {
release {
multiDexKeepFile file('dex.keep')
def myFile = file('dex.keep')
println("isFileExists:"+myFile.exists())
println "dex keep"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
2.在Application中的attachBaseContext方法加入MultiDex.install(base);
public class MyApplication extends Application{
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
// TODO Auto-generated method stub
MultiDex.install(base);
FixDexUtils.loadFixedDex(base);
super.attachBaseContext(base);
}
}
這樣配置之後執行打包出來的apk中有兩個dex檔案:
程式碼處理
1.修復的dex檔案必須要在應用的目錄下面,所以第一步要將修復的檔案移動在/data/data/目錄下面
程式碼處理
1.修復的dex檔案必須要在應用的目錄下面,所以第一步要將修復的檔案移動在/data/data/目錄下面
public static final String DEX_DIR = "odex";
private void fixBug() {
//目錄:/data/data/packageName/odex
File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
//往該目錄下面放置我們修復好的dex檔案。
String name = "classes2.dex";
String filePath = fileDir.getAbsolutePath()+File.separator+name;
File file= new File(filePath);
if(file.exists()){
file.delete();
}
//搬家:把下載好的在SD卡里面的修復了的classes2.dex搬到應用目錄filePath
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+name);
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len=is.read(buffer))!=-1){
os.write(buffer,0,len);
}
File f = new File(filePath);
if(f.exists()){
Toast.makeText(this ,"dex 重寫成功", Toast.LENGTH_SHORT).show();
}
//熱修復
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
}
}
2.掃描dex檔案目錄下的所有dex,並用HashSet儲存。
記得每次初始化工具類的時候清空HashSet的資料
private static HashSet<File> loadedDex = new HashSet<File>();
static{
loadedDex.clear();
}
掃描:
public static void loadFixedDex(Context context){
if(context == null){
return ;
}
//遍歷所有的修復的dex
File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
File[] listFiles = fileDir.listFiles();
for(File file:listFiles){
if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
loadedDex.add(file);//存入集合
}
}
//dex合併之前的dex
doDexInject(context,fileDir,loadedDex);
}
3.獲取需要修復的dex檔案,並通過反射拿到對應的dex陣列,然後一一合併,再通過反射的方式設定給應用的dex陣列中。這樣就實現了熱修復。
private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
File fopt = new File(optimizeDir);
if(!fopt.exists()){
fopt.mkdirs();
}
//1.載入應用程式的dex
try {
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); //原來的dex檔案載入路徑
for (File dex : loadedDex) {
//2.載入指定的修復的dex檔案。
DexClassLoader classLoader = new DexClassLoader(
dex.getAbsolutePath(),//String dexPath,
fopt.getAbsolutePath(),//String optimizedDirectory,
null,//String libraryPath,
pathLoader//ClassLoader parent
);
//3.合併
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathLoader);
Object mDexElementsList = getDexElements(dexObj); //需要修復的dex檔案陣列
Object pathDexElementsList = getDexElements(pathObj); //原來的dex檔案陣列
//合併完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重寫給PathList裡面的lement[] dexElements;賦值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
通過反射設定和獲取值:
//通過反射獲取baseDexClassLoader的pathList
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
//通過反射獲取dexElements
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 獲取某個物件中的屬性
* obj:某個物件
* cl:物件的類
* field:屬性值
**/
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 給某個物件中的新增屬性值
* obj:某個物件
* cl:物件的類
* field:屬性值
* value:具體值
**/
private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj,value);
}
合併陣列:
/**
* 兩個數組合並
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
// [12345] [9876]
// [9876 12345]
備註:
BaseDexClassLoader類中的
DexPathList pathList;
DexPathList類中的
Element[] dexElements;
Element[] dexElements;原來的dex檔案集合
Element[] dexElements2;合併以後的檔案集合
四、測試
通過上面程式碼的處理,應用就可以實現熱修復,那麼該怎麼生成修復的dex檔案呢?
1.找到MyTestClass.class
fixdix_test\app\build\intermediates\bin\TestClass.class
2.配置dx.bat的環境變數
Android\sdk\build-tools\23.0.3\dx.bat
3.命令
dx --dex --output=D:\Users\song\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex
命令解釋:
–output=D:\Users\song\Desktop\dex\classes2.dex 指定輸出路徑
D:\Users\song\Desktop\dex 最後指定去打包哪個目錄下面的class位元組檔案(注意要包括全路徑的資料夾,也可以有多個class)