基于QQ空間熱修複原理實踐
關于熱修複技術,去年真是火的一塌糊塗,俺們沒有及時趕上,好在現在趕上也不算晚,好了廢話不多說,直接進入正題。
-
原理:
簡單闡述一下,具體的還是看原文吧。
說白了這個方案還是在java層的改動,沒有涉及到底層C/C++代碼,還是比較好了解的。說到這裡就不得不提到Android類加載機制。
//DexPathList.java
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses
* reflection to modify 'dexElements' (http://b/7726934).不得不說Facebook好牛叉
*/
private final Element[] dexElements;
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
* 這個方法就是這種熱修複的核心所在:
* 一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個
* dex檔案排列成一個有序的數組dexElements,當加載類的時候,會按順序周遊
* dex檔案,然後從目前周遊的dex檔案中找類,如果找類則傳回,如果找不到
* 從下一個dex檔案繼續查找。
*/
public Class findClass(String name, List<Throwable> suppressed) {
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;
}
理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex檔案的類,如下圖:
是以,熱更新檔方案就是把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:
這就是更新檔修複的基本原理了,當然實作過程中還存在其他問題:方法中直接引用到的類(第一層級關系,不會進行遞歸搜尋)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFIED:
不過也給出了解決方案:
是以為了實作更新檔方案,是以必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED标志。 最終空間的方案是往所有類的構造函數裡面插入了一段代碼,代碼如下:
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex内的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的标志了,隻要沒被打上這個标志的類都可以進行打更新檔操作。
-
熱修複實踐
首先:要來了解2個ClassLoader的子類,
PathClassLoader 用來記載程式的dex;
DexClassLoader 用來加載指定的dex檔案(限制:必須要在應用程式的目錄下面)
public class BaseDexClassLoader extends ClassLoader { //待會利用反射要擷取這個屬性 private final DexPathList pathList; }
- 引用MultiDex分包
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
defaultConfig {
multiDexEnabled true
}
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'
}
}
public class MyApplication extends Application{
@Override
protected void attachBaseContext(Context base) {
// TODO Auto-generated method stub
MultiDex.install(base);
}
}
然後就是核心的處理工具類:
public class FixDexUtils {
//用來存放dex檔案
private static HashSet<File> loadedDex = new HashSet<File>();
//public static final String DEX_DIR = "odex";
// /data/data/packageName/odex dex存放路徑
static{
loadedDex.clear();
}
//在Application中初始化
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);
}
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);
}
//合并dex
private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
// /data/data/packageName/odex/opt_dex
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();
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.合并
//擷取更新檔的pathList屬性
Object dexObj = getPathList(classLoader);
//擷取程式的pathList屬性
Object pathObj = getPathList(pathLoader);
//擷取更新檔的dexElements數組
Object mDexElementsList = getDexElements(dexObj);
//擷取程式的dexElements數組
Object pathDexElementsList = getDexElements(pathObj);
//合并完成,将更新檔dex插入到第一個
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重寫給PathList裡面的Element[] dexElements;指派
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//利用反射機制,擷取cl屬性
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);
}
//利用反射擷取BaseDexClassLoader中的pathList
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
//利用反射擷取DexPathList中的dexElements數組
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"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);
//建立一個length=j的localClass[]數組
Object result = Array.newInstance(localClass, j);
for (int k = ; 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;
}
}
頁面中實作:
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 = ;
byte[] buffer = new byte[];
while ((len=is.read(buffer))!=-){
os.write(buffer,,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();
}
}
-
手動生成classes2.dex
看下怎麼手動生成dex檔案
1,先找到class檔案,javac編譯,或者找IDE編譯好的,MyTestClass.class
dn_fix_ricky_as\app\build\intermediates\bin\MyTestClass.class
2,dx.bat指令生成dex檔案
dx –dex –output=D:\Users\ricky\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex
指令解釋:
–output=D:\Users\ricky\Desktop\dex\classes2.dex 指定輸出路徑
D:\Users\ricky\Desktop\dex 最後指定去打包哪個目錄下面的class位元組檔案(注意要包括全路徑的檔案夾,也可以有多個class)
參考:
1. 安卓App熱更新檔動态修複技術介紹
2. DexPathList源碼
3. BaseDexClassLoader源碼
4. Android4.4.2 DexClassLoader源碼分析
5. 美團Android DEX自動拆包及動态加載簡介