Android性能優化彙總
熱修復
-
阿里系:DeXposed。andfix
從底層C的二進制來入手的。 -
騰訊系:tinker
Java類加載機制來入手的。
一 什麼是熱修復?
一般的bug修復,都是等下一個版本解決,然後發佈新的apk。
熱修復: 可以直接在客戶已經安裝的程序當中修復bug。bug一般會出現在某個類的某個方法地方。我們需要動態地將客戶手機裏面的apk裏面的某個類給替換成我們已經修復好的類。
二 原理
- java的類加載機制:
安卓系統通過classLoader加載dex文件。
public class PathClassLoader extends BaseDexClassLoader {
用來加載應用程序的dex
public class DexClassLoader extends BaseDexClassLoader {
可以加載指定的某個dex文件。(限制:必須要在應用程序的目錄下面)
-
系統通過BaseDexClassLoader類 中的DexPathList,再通過DexPathList類中的Element[] dexElements,找到對應的第一個java類,從而進行類的加載;所以,我們只需把我們修改後的dex文件元素利用反射技術,插入到dexElements數組的中去,並在數組的前面。這樣,程序在運行時,會先拿到我們修復的類,運行無異常,從而實現了修復
-
安卓java文件最終打包生產dex文件,我們修改bug後的java文件要編譯成dex文件,然後去“替換”已經存在的dex文件中對應的那個類
-
通過tools文件夾下工具可以將java文件打包生產dex文件
三 實現方式
- 修改後的java文件,編譯生成dex文件
- io流讀入文件到應用中
- classLoader加載文件獲取加載的dex文件中對應的元素
- 將加載進來的元素合併到已經存在的DexPathList的Element[] dexElements 中去
四 主要的dex文件加載代碼
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);
}
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);
}
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();
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);
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");
}
/**
* 兩個數組合並
* @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]
}
五 使用
1 Activity中報錯
MyTestClass 報錯了,a= 0,a不能作爲分母
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 0;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
Activity中使用了MyTestClass
public class Lsn16Activity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.lsn16_acitivy);
}
public void test(View v) {
MyTestClass myTestClass = new MyTestClass();
myTestClass.testFix(this);
}
}
2 定義一個修復的方法
public void fix(View v) {
//目錄:/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();
}
}
3 修改報錯的地方
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 1;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
4 編譯MyTestClass.class 生成dex文件
5 加載dex文件
實際運用中通過網絡請求,從服務器中獲取dex文件,這裏用點擊事件fix()方法代替