1. 概述
前邊我們分析并寫了阿里的熱修復方法,可以知道阿里的熱修復是不能增加成員變量甘耿、成員方法和資源的,所以基于這個原因,然后我們上節(jié)課又通過對類的加載流程的源碼做了一個分析夸赫,那么我們這節(jié)課就來看下我們自己的一個修復的方法,其實很簡單咖城,就是鉆了一個空子茬腿,說白了,就是根據這幾個弊端以及類的加載流程然后得出自己的一個熱修復的方法宜雀,如果沒有看的可以先去看下我的這兩篇文章:
Android熱修復打補丁技術 - (阿里熱修復生成補丁包)
類的加載流程源碼分析
2. 回顧阿里熱修復流程和類的加載流程
2.1>:首先先來回顧下阿里的熱修復流程切平,流程圖如下:
分析如下:
1>:把我們之前有bug的版本打一個包,我們叫做 old.apk辐董,然后我們修復該bug悴品,現(xiàn)在是沒有bug的,叫做 new.apk;
2>:在客戶端去阿里官網下載 AndFix苔严,然后生成一個 fix.apatch差分包定枷,然后把這個差分包放在我們自己的服務器,讓用戶去下載届氢;
3>:當用戶把app只要一打開欠窒,檢測到有bug的話,就會去服務器下載這個差分包退子,然后我們就會在 BaseApplication中調用 addPatch()方法去修復bug岖妄;
生成差分包的方法:
1.命令是:
apkpatch.bat -f <new> -t <old> -o <output> -k <keystore> -p <*> -a <alias> -e <*>
-f : 沒有Bug的新版本apk.
-t : 有bug的舊版本apk
-o : 生成的補丁文件所放的文件夾
-k : 簽名打包密鑰
-p : 簽名打包密鑰密碼
-a : 簽名密鑰別名
-e : 簽名別名密碼(這樣一般和密鑰密碼一致)
我的:apkpatch.bat -f new.apk -t old.apk -o out -k joke.jks -p 123456 -a test -e 123456
然后點擊回車,會在out中生成一個 xxx.apatch文件寂祥,將 xxx.apatch文件重名為 fix.apatch荐虐;
以上就是阿里熱修復的一個流程;
2.2>:回顧類的加載流程丸凭,分析流程圖如下:
分析如下:場景:從MainActivity啟動一個 TestActivity
首先先來看下繼承關系:
PathClassLoader --> BaseDexClassLoader --> ClassLoader
1>:首先會去找 PathClassLoader福扬,然后會去找BaseDexClassLoader,然后會去找 ClassLoader惜犀;
2>:然后調用 ClassLoader中的findClass()方法忧换,但是由于 子類 BaseDexClassLoader覆蓋了父類的該方法,所以這里就調用的是 子類BaseDexClassLoader的findClass()方法向拆,調用方法如下亚茬;
3>:由上邊方法可以看出,其實是調用的 pathList.findClass()方法浓恳,而pathList它就是 DexPathList類刹缝,可以發(fā)現(xiàn)它里邊的 findClass()方法如下:
4>:可以看出, DexPathList中的 findClass()方法其實就是 通過for循環(huán)遍歷 app中所有的 dexElements的數組颈将,只要找到了 class梢夯,這里就是說只要找到了 TestActivity的這個class,那么就直接返回 一個 class給 PathClassLoader晴圾,然后 通過 (Acitivty)cl.loadClass(className).newInstance()方法颂砸,其實就是通過反射去創(chuàng)建對象;
以上就是類的加載流程分析
那么基于上邊的分析死姚,下邊我們就來看下我們這節(jié)課所要講解的一個我們自己的熱修復方法人乓。
3. 自己的熱修復方法,流程圖如下:
分析上圖可知:
我們其實所采用的方式就是:
1>:首先先去修復好這個bug都毒,然后打一個apk包色罚,然后把后綴名改為 .zip并且解壓,解壓后會有一個 class.dex文件账劲,這里需要手動去把這個文件重命名為 fix.dex戳护,然后把這個 fix.dex文件放到服務器中金抡;
2>:用戶手中的apk是有bug的,上圖的左邊就是我們 app中所有的 dex文件腌且,比如說我們把有bug的類暫且就叫做 bug.class梗肝,比如說它就在 所有 dex文件的最前邊;
3>:然后我們只需要把這個 已經修復的 fix.class文件插入到 有bug.class的最前邊就可以铺董,由 類的加載流程可以知道巫击,在 DexPathList中的findClass()方法中,會for循環(huán)遍歷class柄粹,這里就是需要去找到 TestActivity喘鸟,只要找到后就直接 return class匆绣,就不會去往后邊去找驻右,所以也就不會去找 后邊有bug的class了,所以就達到了修復的一個目的崎淳;
這樣的話堪夭,只要修復bug了,那么每次就不會再去找后邊有bug的類了拣凹。
注意:自己手中的app一定是有bug的森爽,而服務器上邊的是沒有bug的,只要用戶一打開app嚣镜,就會去下載整個 dex包爬迟,然后進行插入修復。
4. 修復后的最終效果
下圖就是我們采用我們自己的修復方法的修復結果菊匿,只要用戶第一次修復成功后付呕,那么以后每次進入app之后就都會提示修復成功的,就不會去找之前有bug的class類跌捆,這個我們在上邊也都是說過的徽职;
5. 具體代碼如下
5.1>:修復的代碼如下:
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/5 9:49
* Version 1.0
* Params:
* Description: 修復
*/
public class FixDexManager {
private Context mContext ;
private File mDexDir ;
public FixDexManager(Context context) {
this.mContext = context ;
// 獲取應用可以訪問的 dex目錄
this.mDexDir = context.getDir("odex" , Context.MODE_PRIVATE) ;
}
/**
* 修復dex包
* fixDexPath:下載補丁的路徑
*/
public void fixDex(String fixDexPath) throws Exception {
// 2. 然后獲取下載好的補丁 dexElement
// 2.1 把fixDexPath移動到 系統(tǒng)能夠訪問的 dex目錄下,因為我們最終要把它變?yōu)?ClassLoader
File srcFile = new File(fixDexPath) ;
if (!srcFile.exists()){
throw new FileNotFoundException(fixDexPath) ;
}
File destFile = new File(mDexDir , srcFile.getName()) ;
if (destFile.exists()){
Log.d(TAG, "patch [" + fixDexPath + "] has be loaded.");
return;
}
copyFile(srcFile , destFile);
// 2.2 讓該ClassLoader讀取 fixDex路徑
//需要修復的文件
List<File> fixDexFiles = new ArrayList<>() ;
fixDexFiles.add(destFile) ;
fixDexFiles(fixDexFiles) ;
}
/**
* 把合并的數組applicationDexElements 注入到 原來的 applicationClassLoader中即可
*/
private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception {
// 1. 先獲取 pathList(通過反射)
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList") ;
// private佩厚、public姆钉、protected都可以反射
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader) ;
// 2. 獲取pathList里邊的 dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// 合并
dexElementsField.set(pathList , dexElements);
}
/**
* 合并兩個數組
*
* @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;
}
/**
* copy file
*
* @param src source file
* @param dest target file
* @throws IOException
*/
public static void copyFile(File src, File dest) throws IOException {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
if (!dest.exists()) {
dest.createNewFile();
}
inChannel = new FileInputStream(src).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
} finally {
if (inChannel != null) {
inChannel.close();
}
if (outChannel != null) {
outChannel.close();
}
}
}
/**
* 從 ClassLoader中 獲取 dexElements
*/
private Object getDexElementsByClassLoader(ClassLoader classLoader) throws Exception {
// 1. 先獲取 pathList(通過反射)
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList") ; // pathList是源碼中的
// private、public抄瓦、protected都可以反射
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader) ;
// 2. 獲取pathList里邊的 dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements"); // dexElements是源碼中的
dexElementsField.setAccessible(true);
return dexElementsField.get(pathList);
}
/**
* 加載全部的修復包
*/
public void loadFixDex() throws Exception {
File[] dexFiles = mDexDir.listFiles() ;
List<File> fixDexFiles = new ArrayList<>() ;
for (File dexFile : dexFiles) {
if (dexFile.getName().endsWith(".dex")){
fixDexFiles.add(dexFile) ;
}
}
fixDexFiles(fixDexFiles) ;
}
/**
* 修復 dex
*/
private void fixDexFiles(List<File> fixDexFiles) throws Exception {
// 1. 先獲取應用的 已經運行的 dexElements
ClassLoader applicationClassLoader = mContext.getClassLoader();
Object applicationDexElements = getDexElementsByClassLoader(applicationClassLoader) ;
File optimizedDirectory = new File(mDexDir , "odex") ;
if (!optimizedDirectory.exists()){
optimizedDirectory.mkdirs() ;
}
// 開始修復
for (File fixDexFile : fixDexFiles) {
// dexPath:修復dex的路徑 --> fixDexFiles
// optimizedDirectory:解壓路徑
// libraryPath:so文件的路徑
// parent:父的ClassLoader
ClassLoader fixDexClassLoader = new BaseDexClassLoader(
fixDexFile.getAbsolutePath() , // 修復的dex的路徑 必須要在應用目錄下的odex文件中
optimizedDirectory, // 解壓路徑
null , // .so文件的路徑
applicationClassLoader // 父的 ClassLoader
) ;
// 獲取app中的 dexElements數組潮瓶,然后解析來就需要把 下載的沒有bug的 補丁的 dexElement插入到這個數組的最前邊
Object fixDexElements = getDexElementsByClassLoader(fixDexClassLoader) ;
// 3. 把補丁的 dexElement插到 已經運行的 dexElement的最前面,其實就是合并钙姊,修復就ok
// applicationClassLoader 是一個數組筋讨,fixDexElements也是一個數組,就是把兩個數組合并
// 3.1 合并完成
// 前者是修復的摸恍,后者是沒有修復的
applicationDexElements = combineArray(fixDexElements , applicationDexElements) ;
}
// 3.2 把合并的數組注入到 原來的 applicationClassLoader中即可
injectDexElements(applicationClassLoader , applicationDexElements) ;
}
}
5.2>:在MainActivity中的initData()方法中直接調用:
public class MainActivity extends BaseSkinActivity {
@ViewById(R.id.btn_test)
Button btn_test ;
@Override
protected void setContentView() {
setContentView(R.layout.activity_main);
}
@Override
protected void initTitle() {
}
@Override
protected void initView() {
btn_test.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(TestActvity.class);
}
});
}
@Override
protected void initData() {
// 用戶只要一打開app悉罕,就去調用我們自己的修復方式
fixDexBug() ;
}
private void fixDexBug() {
File fixFile = new File(Environment.getExternalStorageDirectory() , "fix.dex") ;
if (fixFile.exists()) {
FixDexManager fixDexManager = new FixDexManager(this);
try {
fixDexManager.fixDex(fixFile.getAbsolutePath()) ;
Toast.makeText(MainActivity.this , "修復成功" , Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(MainActivity.this , "修復失敗" , Toast.LENGTH_SHORT).show();
}
}
}
}
5.3>:最后需要在BaseApplication中調用之前所有修復的 dex包赤屋;
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/1 12:48
* Version 1.0
* Params:
* Description:
*/
public class BaseApplication extends Application {
public static PatchManager mPatchManager ;
@Override
public void onCreate() {
super.onCreate();
// 加載所有修復的 dex包
try {
FixDexManager fixDexManager = new FixDexManager(this) ;
fixDexManager.loadFixDex() ;
} catch (Exception e) {
e.printStackTrace();
}
}
}
6. 開發(fā)中的一些細節(jié)
1>:可以把出錯的 class 重新單獨的打成 一個 fix.dex,在這里就指的是 TestActivity壁袄,大小也比較小类早,不過不太可取,除非說代碼不混淆嗜逻;
2>:可以采用分包涩僻,可以把不會出錯的類打成一個 dex(這里的不要混淆),有錯的留在另一個 dex中(這里可以混淆)栈顷,但是如果方法數沒有超過 65536逆日,系統(tǒng)默認不會給你分包,那么你需要去Android Studio官網找分包萄凤,而且運行的時候如果 dex過大會影響啟動速度室抽;
3>:直接把整個項目打成apk,然后修改后綴名為 zip并且去解壓靡努,然后修改里邊的class.dex文件名為 fix.dex坪圾,然后把fix.dex文件放在服務器,只要用戶一打開app惑朦,就會去下載整個 dex包兽泄,然后進行插入修復,但也會導致一個問題漾月,就是下載的 補丁的 fix.dex大小可能比較大病梢,2M左右;
一般就用 第3種方法
和阿里的相比較梁肿,阿里的不能增加成員變量和成員方法蜓陌,而我們的可以增加成員變量、成員方法栈雳、類护奈,但是不能增加資源(騰訊的可以增加資源)
代碼已上傳至github:
https://github.com/shuai999/EssayJoke_day_04.git