前言:
当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。
这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?
虽然Android系统并没有提供这个技术,但是很幸运的告诉大家,答案是:可以,Android可以使用热补丁动态修复技术来解决以上这些问题。
解决方案
简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是androiddex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?
ClassLoader机制
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:
技术实现:
创建DexLoaderUtil 类,默认加载Asset中包含定义控件BeatView的Dex包,如果Sdcard 根目录下有Dex,则优先加载sdcard
public class DexLoaderUtil {
private static final String TAG = "DexLoaderUtil";
public static final String HOTLIB3_DEX_NAME = "HotLib3.dex";
public static final String HOTLIB3_CLASS_NAME = "com.example.hotlib.BeatView";
//public static final String HOTLIB3_CLASS_NAME_FIX = "com.example.hotlib.BugClass";
private static final int BUF_SIZE = 8 * 1024;
private static final String DEX_PATH = "sdcard/";
public static String getDexPath(Context context, String dexName) {
return new File(context.getDir("dex", Context.MODE_PRIVATE), dexName)
.getAbsolutePath();
}
public static String getOptimizedDexPath(Context context) {
return context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath();
}
public static boolean copysdCardDex(Context context, String dexName) {
boolean bRes = false;
File dexInternalStoragePath = new File(context.getDir("dex",
Context.MODE_PRIVATE), dexName);
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try {
File file = new File(DEX_PATH + dexName);
bis = new BufferedInputStream(new FileInputStream(file));
// bis = new BufferedInputStream(context.getAssets().open(dexName));
dexWriter = new BufferedOutputStream(new FileOutputStream(
dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
bRes = true;
} catch (FileNotFoundException e) {
Log.d("Qinghua", e.getMessage());
e.printStackTrace();
} catch (IOException e) {
Log.d("Qinghua", e.getMessage());
e.printStackTrace();
}
Log.d("Qinghua", DEX_PATH + dexName + " " + bRes);
return bRes;
}
public static void copyDex(Context context, String dexName) {
if (!copysdCardDex(context, dexName)) {
File dexInternalStoragePath = new File(context.getDir("dex",
Context.MODE_PRIVATE), dexName);
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try {
// File file = new File(DEX_PATH+dexName);
// bis = new BufferedInputStream(new FileInputStream(file));
bis = new BufferedInputStream(context.getAssets().open(dexName));
dexWriter = new BufferedOutputStream(new FileOutputStream(
dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
} catch (FileNotFoundException e) {
Log.d("Qinghua", e.getMessage());
e.printStackTrace();
} catch (IOException e) {
Log.d("Qinghua", e.getMessage());
e.printStackTrace();
}
}
}
public static void loadAndCall(Context context, String dexName,
String className) {
final File dexInternalStoragePath = new File(context.getDir("dex",
Context.MODE_PRIVATE), dexName);
final File optimizedDexOutputPath = context.getDir("outdex",
Context.MODE_PRIVATE);
DexClassLoader cl = new DexClassLoader(
dexInternalStoragePath.getAbsolutePath(),
optimizedDexOutputPath.getAbsolutePath(), null,
context.getClassLoader());
call(cl, className);
}
public static void call(ClassLoader cl, String className, Object instance,
String functionName) {
// String str = "";
Class myClasz = null;
try {
myClasz = cl.loadClass(className);
myClasz.getDeclaredMethod(functionName).invoke(instance);
// .toString();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// return str;
}
public static Object callInstance(ClassLoader cl, String className,
Context para) {
Object str = null;
Class myClasz = null;
try {
myClasz = cl.loadClass(className);
// Object instance
str = myClasz.getConstructor(Context.class).newInstance(para);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return str;
}
public static String call(ClassLoader cl, String className) {
String str = "";
Class myClasz = null;
try {
myClasz = cl.loadClass(className);
Object instance = myClasz.getConstructor().newInstance();
// str = myClasz.getDeclaredMethod("test1",
// String.class).invoke(instance, "cissy").toString();
str = myClasz.getDeclaredMethod("test1").invoke(instance)
.toString();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return str;
}
public static synchronized Boolean injectAboveEqualApiLevel14(
String dexPath, String defaultDexOptPath, String nativeLibPath,
String dummyClassName) {
Log.i(TAG, "--> injectAboveEqualApiLevel14");
PathClassLoader pathClassLoader = (PathClassLoader) DexLoaderUtil.class
.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
defaultDexOptPath, nativeLibPath, pathClassLoader);
try {
dexClassLoader.loadClass(dummyClassName);
Object dexElements = combineArray(
getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
} catch (Throwable e) {
e.printStackTrace();
return false;
}
Log.i(TAG, "<-- injectAboveEqualApiLevel14 End.");
return true;
}
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader,
Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
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 void setField(Object obj, Class<?> cl, String field,
Object value) throws NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}
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;
}
}
Activity Class 调用:
public class MainActivity extends ActionBarActivity {
private Button dowork3;
Context ctx;
RelativeLayout rl;
private updateDatabaseReceiver mUpdateDatabaseReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ctx = this;
new innitDexThread().start();
dowork3 = (Button) findViewById(R.id.work3);
dowork3.setText("beatview start");
dowork3.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
DexLoaderUtil.call(getClassLoader(),
DexLoaderUtil.HOTLIB3_CLASS_NAME, rl.getChildAt(0),
"start");
}
});
rl = (RelativeLayout) findViewById(R.id.beatviewP);
}
Handler loadHnadler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0: {
Toast to = Toast.makeText(getApplicationContext(), String
.format("Init dex file:%s done!",
DexLoaderUtil.HOTLIB3_DEX_NAME),
Toast.LENGTH_SHORT);
to.show();
new accSouThreadBeatInit(ctx).start();
break;
}
case 3: {
Toast to = Toast.makeText(getApplicationContext(),
"init beatview done!", Toast.LENGTH_SHORT);
to.show();
rl.addView((View) msg.obj);
break;
}
default:
}
}
};
public class accSouThreadBeatInit extends Thread {
Context mctx;
public accSouThreadBeatInit(Context mctxP) {
this.mctx = mctxP;
}
@Override
public void run() {
super.run();
Object obj = DexLoaderUtil.callInstance(getClassLoader(),
DexLoaderUtil.HOTLIB3_CLASS_NAME, mctx);
Message msg = loadHnadler.obtainMessage();
if (null == obj) {
Log.e("Qinghua", "xxxxxxxxxxxxxxxxxxx");
}
msg.what = 3;
msg.obj = obj;
// msg.what = P.SEARCHCOMPLETE;
loadHnadler.sendMessageDelayed(msg, 20);
}
}
public class innitDexThread extends Thread {
@Override
public void run() {
super.run();
if (!GerSharePerferencesInfo.getDexUpdated(getApplicationContext())) {
DexLoaderUtil.copyDex(MainActivity.this,
DexLoaderUtil.HOTLIB3_DEX_NAME);
GerSharePerferencesInfo.setDexUpdated(getApplicationContext(),
true);
}
String HotLib3DexPath = DexLoaderUtil.getDexPath(MainActivity.this,
DexLoaderUtil.HOTLIB3_DEX_NAME);
DexLoaderUtil.injectAboveEqualApiLevel14(HotLib3DexPath,
optimizedDexOutputPath, null,
DexLoaderUtil.HOTLIB3_CLASS_NAME);
Message msg = loadHnadler.obtainMessage();
msg.what = 0;
loadHnadler.sendMessageDelayed(msg, 20);
}
}
}
效果:
补丁更新
现在考虑到BeatView显示每秒帧数高,CPU占用率大,修改BeatView Code修复这个bug,重新包Dex,并放在Sdcard要目录下,去做热补丁更新:
重新启动APP的效果:
结果:
BeatView帧数降低了,整个过程中没有重新安装APP 或做Update,在用户简单的重启APP过程中就修复了Bug。
结语:
现实项目中做Dex补丁更新,还需要了解相关的分包方案,网络推送,版本控制,参数设定(Google Tag Manager)等具体细节,每一个展开说都很有内涵,本文不再细述。