一般產品上線週期比較長,而且如果不是強制更新,無法做到100%的使用者都更新,如果上線之後,產品出現bug,那麼怎麼辦?一般都是再發一個版本,或者等到下一個版本再解決。如果再發一個版本,顯然是不靠譜的,使用者安裝也有厭倦的時候,說不定直接把app給解除安裝了,而且給使用者的體驗也不好。如果等到下一個版本再修復,那麼使用者每次使用都出現這個bug,顯然是不能接受的,這都造成使用者量流失,給我們使用者造成不良的影響。
那麼有沒有一種辦法是不需要釋出版本,不需要使用者安裝,而且又能夠及時覺得這個問題的呢?當然是有的,那就是使用熱修復技術。
熱修復技術一般來說有兩種途徑來解決:
1、阿里系的 AndFix,原理是從底層C的二進位制來入手的。
2、騰訊系的 tinker,原理是Java類載入機制來入手的。 當然熱修復技術不單單這兩種,各大公司都開源自己的熱修復技術,如滴滴的 VirtualAPK,美團的Robust等等。
既然有那麼多的熱修復專案開源,讀者可以下載下來學習,看看實現的原理,本文主要是講解從Java類載入器機制來講解熱修復。
首先我們理解DexClassLoader和PathClassLoader,他們都是用來載入應用程式的dex檔案,但DexClassLoader是指可以載入指定的某個dex檔案,而且具有限制性,那就是必須要在應用程式的目錄下面的dex檔案。
dexPath:包含classes和resources的jar/apk檔案的路徑; libraryPath:包含本地的目錄列表,可以為null; parent:父類載入器。
dexPath:包含classes和resources的jar/apk檔案的路徑; optimizedDirectory:寫入優化的dex檔案的目錄,一定不能是為空; libraryPath:包含本地的目錄列表,可以為null; parent:父類載入器。
DexClassLoader和PathClassLoader類中都只有構造方法,而且構造方法都super到父類了。我們看下BaseDexClassLoader的類載入器的構造方法。
BaseDexClassLoader類中包括構造方法,findClass(找到類)、findResourece,findLibrary等等方法,這裡主要看下構造方法。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
複製程式碼
主要這裡new了一個DexPathList物件,並且把引數傳過去了,DexPathList到底是幹什麼的呢?我們去探個究竟。
類的簡介中我們知道,DexPathList其實就是與類載入器(ClassLoader)相關聯的一對條目列表。而這些列表包含了dex、jar、zip和apk
檔案,而提高了使用列表來查詢類和資源的方法。這裡需要說一下的是平時我們看原始碼一定要主要看作者的註釋,從註釋裡我們看知道類或者方法的功能。
從DexPathList原始碼中我們注意到一個屬性就是dexElements
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;
複製程式碼
這個就是存放dex/resource的集合,我們注意到該類的findClass方法:
findClass的時候就是遍歷dexElements,取出DexFile,找到我們需要的類並返回。也就是說我們可以通過dexElements找到我們出bug的類,因為dexElements就是存放Element元素,而Element中包含DexFile,DexFile中可以找到bug類,找到bug類,然後我們替換掉bug類所在的dex檔案就可以了。而且再dexElements介紹中,已經說明了可以通過反射獲取得到dexElements。
那麼我們思考一下:既然DexClassLoader和PathClassLoader都可以載入dex檔案,那麼我們能不能使用多個dex檔案?也就是第一個版本使用的是classes.dex,修復後的補丁包classes2.dex,在補丁包中包涵了我們修復class檔案。然後將這兩個檔案合併,將修復的class替換原來出bug的class,接下來通過反射拿到dexElements,將修復好的dex插入到dexElements的集合,插入的位置就是出現bug的class所在的dex
的前面。這樣子做最本質的實現原理就是:類載入器去載入某個類的時候,是去dexElements裡面從頭往下查詢的。
需要多個dex的需要multidex支援。 1、再app的build.gradle中新增
defaultConfig {
multiDexEnabled true
}
buildTypes {
release {
multiDexKeepFile file('dex.keep')
println "dex keep"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
複製程式碼
2、在application的attachBaseContext新增MultiDex.install(base);
據上面的原理,下面是實現的熱修復的程式碼
FixDexUtils:
public class FixDexUtils {
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);
}
//使用合併的dexElements替換出錯的dexElements
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);
}
// fileDir = {File@15225} "/data/data/com.dex.main/app_odex"
// 0 = {File@15240} "/data/data/com.dex.main/app_odex/opt_dex"
// 1 = {File@15241} "/data/data/com.dex.main/app_odex/classes2.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();
}
try {
//1.使用PathClassLoader載入應用程式的dex(系統的載入器)
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
//2.通過DexClassLoader載入指定的修復的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);
Object pathDexElementsList = getDexElements(pathObj);
//合併完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重寫給PathList裡面的lement[] dexElements;賦值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
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);
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 兩個陣列合並
* 有bug的dex還是保留,只是把沒bug的插入到有bug的前面即可
* @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);//新建一個陣列,總長度為兩個陣列的總和,即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;//返回總的陣列
}
}
複製程式碼
MyApplication:
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);
}
}
複製程式碼
MyConstants:
public class MyConstants {
public static final String DEX_DIR = "odex";
}
複製程式碼
MyTestClass:
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 0;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
複製程式碼
MainActivity:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void test(View v){
MyTestClass myTestClass = new MyTestClass();
myTestClass.testFix(this);
}
public void fix(View v){
fixBug();
}
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 (fileDir.exists()) {
if (file.exists()) {
file.delete();
}
}
//搬家:把下載好的在SD卡里面的修復了的classes2.dex搬到應用目錄filePath
InputStream is = null;
FileOutputStream os = null;
try {
String dpath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator;
is = new FileInputStream(dpath+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();
}
}
}
複製程式碼
activity_main:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="test"
android:onClick="test"
android:layout_centerInParent="true"
android:id="@+id/btn_test"
/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="fix"
android:onClick="fix"
android:layout_below="@id/btn_test"
/>
</RelativeLayout>
複製程式碼
編譯dex檔案
1、修復好bug,然後rebuild一下,獲取java檔案對應的所有class檔案(即找到修復好的java檔案所對應的calss檔案):app\build\intermediates\classes\debug\包路徑\類名字,然後右鍵show in Explorer。把檔案複製出來,複製的時候連同整個包名路徑(即帶包名的整個資料夾)都複製出來。(也可以找到class檔案之後選擇壓縮,注意壓縮時選擇絕對路徑,再解壓可保留原有的包目錄)
2、cmd到sdk的build-tools\sdk版本
目錄下。
3、使用命令dx --dex --output=D:\Users\dex\classes2.dex D:\Users\dex
命令解釋:
--output=D:\Users\dex\classes2.dex 指定輸出路徑和檔名
D:\Users\dex` 最後指定去打包哪個目錄下面的class位元組檔案(注意:要包括全路徑的資料夾,也可以有多個class)
可以看到我這個dex資料夾下有包含全包名路徑的資料夾,該資料夾裡面有class檔案。
當我們點選第一次進來點選test的時候,會閃退,第二次進來,我們點選fix時候,已經把bug修復好了,這時候就不會閃退了。
參考文章: 《Android4.4.2 DexClassLoader原始碼分析》
BaseDexClassLoader原始碼連線:BaseDexClassLoader
DexPathList原始碼連線:DexPathList