@linux1s1s
2015-09-01T08:46:35.000000Z
字数 8548
阅读 4095
AndroidBuild
Android 客户端应用上线以后,一旦出现Bug,一般的解决思路是发修复包升级应用,这种方式不仅耗时,更重要的是用户需要频繁的升级版本,体验不好,所以优化的思路是在不发版本的情况下热更新,以期提高用户体验。
近期GitHub新出一种非侵入运行期AOP框架Dexposed, 下面简单了解一下这个框架,GitHub地址。
该框架基于AOP思想,支持经典的AOP使用场景,可应用于日志记录,性能统计,安全控制,事务处理,异常处理等方面。
针对Android平台,Dexposed支持函数级别的在线热更新,例如对已经发布在应用市场上的宿主APK,当我们从crash统计平台上发现某个函数调用有bug,导致经常性crash,这时,可以在本地开发一个补丁APK,并发布到服务器中,宿主APK下载这个补丁APK并集成后,就可以很容易修复这个crash。
Dexposed是基于久负盛名的开源Xposed框架实现的一个Android平台上功能强大的无侵入式运行时AOP框架。Dexposed的AOP实现是完全非侵入式的,没有使用任何注解处理器,编织器或者字节码重写器。
首先从GitHub上拉下来代码有几个坑需要注意:
接下来我们看看具体的流程:
首先需要我们动态监测AOP环境
runPatchApk();
这里需要注意的是PatchMain.load()这个方法,该方法的主要用途是加载patch APK的所有类,并将实现IPatch的类添加到List中去,然后通过匹配加载的类或者类方法来实现非侵入式AOP。
public void runPatchApk() {if (android.os.Build.VERSION.SDK_INT == 21) {return;}if (!DexposedBridge.canDexposed(this)) {Log.d("Hotpatch", "This device doesn't support dexposed!");return;}File cacheDir = getExternalCacheDir();if (cacheDir != null) {String fullpath = cacheDir.getAbsolutePath() + File.separator + "PATCH_NAME.apk";PatchResult result = PatchMain.load(this, fullpath, null);if (result.isSuccess()) {Log.e("Hotpatch", "patch success!");} else {Log.e("Hotpatch", "patch error is " + result.getErrorInfo());}}}
DexposedBridge.canDexposed(this)
public static synchronized boolean canDexposed(Context context) {return !DeviceCheck.isDeviceSupport(context)?false:loadDexposedLib(context);}private static boolean loadDexposedLib(Context context) {try {if(VERSION.SDK_INT != 10 && VERSION.SDK_INT != 9) {if(VERSION.SDK_INT > 19) {System.loadLibrary("dexposed_l");} else {System.loadLibrary("dexposed");}} else {System.loadLibrary("dexposed2.3");}return true;} catch (Throwable var2) {return false;}}
public class Activity extends BaseSherlockSubActivity implementsOnNewIconUIRefreshListener {private void showDialog() {final AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("Dexposed sample").setMessage("Please clone patchsample project to generate apk, and copy it to \"/Android/data/PACKAGE_NAME/cache/PATCH_NAME.apk\"").setPositiveButton("ok", new DialogInterface.OnClickListener() {public void onClick(DialogInterface dialog, int whichButton) {}}).create().show();}}
假如我们上线的代码中如上所示,在弹层中出现文案bug,那么该如何热更新。
代码修复操作在Patch工程中,添加如下代码:
public class DialogPatch implements IPatch {@Overridepublic void handlePatch(final PatchParam arg0) throws Throwable {Class<?> cls = null;try {cls = arg0.context.getClassLoader().loadClass("com.android.activity.Activity");} catch (ClassNotFoundException e) {e.printStackTrace();return;}DexposedBridge.findAndHookMethod(cls, "showDialog", new XC_MethodReplacement() {@Overrideprotected Object replaceHookedMethod(MethodHookParam param) throws Throwable {final Activity mainActivity = (Activity) param.thisObject;AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);builder.setTitle("Fanli Dexposed sample").setMessage("The dialog is shown from patch apk!").setPositiveButton("ok", new OnClickListener() {public void onClick(DialogInterface dialog, int whichButton) {Class<?> clsInner;try {clsInner = arg0.context.getClassLoader().loadClass("com.android.activity.OutObject");} catch (ClassNotFoundException e) {e.printStackTrace();return;}try {OutObject outObject = (OutObject) clsInner.newInstance();if (outObject.callFromOutMethod()) {AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);builder.setTitle("Fanli Dexposed sample").setMessage("com.android.activity.OutObject is Worked!").setPositiveButton("ok", new OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {dialog.dismiss();}}).create().show();}} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}}).create().show();return null;}});}}
然后将这个patch APK 传到Server,在主APK中通过下载patch apk到指定目录,然后动态监测AOP环境并loadPatch即可实现热更新。
接下来如果应用到实际项目中需要完善的有以下几点:
基于以上实现方案测试的环境包括:
Dalvik 4.0-4.4均已经通过
目前 ART 5.0 以及以上版本 尚未通过。(待更新Native包和Jar包)
- 静态变量无法做补丁,因为静态变量初始化在补丁初始化之前
- 涉及到动态链接库的调用无法做补丁
- 补丁模块应用在Application初始化的时候,无法在app运行时热替换,只能等下次启动或切后台后启动
- 补丁方式是以Context类作为切入点替换,补丁中所有相关类的替换需要带上Context类
- Application类无法替换
- 静态变量可以做补丁,但是final型变量无法做补丁。
- 涉及到动态链接库的调用可以引入Jar,然后在做相应的补丁操作。
- 可以在运行时做热更新,时间精度自己控制。
- Context取Application Context,不需要其他Context。
- Application类可以通过替换其中的方法完成类的替换。
public class PatchMain {private static final ReadWriteSet<PatchCallback> loadedPatchCallbacks = new ReadWriteSet<PatchCallback>();/*** Load a runnable patch apk.** @param context the application or activity context.* @param apkPath the path of patch apk file.* @param contentMap the object maps that will be used by patch classes.* @return PatchResult include if success or error detail.*/public static PatchResult load(Context context, String apkPath, HashMap<String, Object> contentMap) {if (!new File(apkPath).exists()) {return new PatchResult(false, PatchResult.FILE_NOT_FOUND, "FILE not found on " + apkPath);}PatchResult result = loadAllCallbacks(context, apkPath, context.getClassLoader());if (!result.isSuccess()) {return result;}if (loadedPatchCallbacks.getSize() == 0) {return new PatchResult(false, PatchResult.NO_PATCH_CLASS_HANDLE, "No patch class to be handle");}PatchParam lpparam = new PatchParam(loadedPatchCallbacks);lpparam.context = context;lpparam.contentMap = contentMap;return PatchCallback.callAll(lpparam);}private static PatchResult loadAllCallbacks(Context context, String apkPath, ClassLoader cl) {try {// String dexPath = new File(context.getFilesDir(), apkPath.).getAbsolutePath();File dexoptFile = new File(apkPath + "odex");if (dexoptFile.exists()) {dexoptFile.delete();}ClassLoader mcl = null;try {mcl = new DexClassLoader(apkPath, context.getFilesDir().getAbsolutePath(), null, cl);} catch (Throwable e) {return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);}DexFile dexFile = DexFile.loadDex(apkPath, context.getFilesDir().getAbsolutePath() + File.separator + "patch.odex", 0);Enumeration<String> entrys = dexFile.entries();// clean old callbacksynchronized (loadedPatchCallbacks) {loadedPatchCallbacks.clear();}while (entrys.hasMoreElements()) {String entry = entrys.nextElement();Class<?> entryClass = null;try {entryClass = mcl.loadClass(entry);} catch (ClassNotFoundException e) {e.printStackTrace();break;}catch (IllegalAccessError e) {e.printStackTrace();break;}if (isImplementInterface(entryClass, IPatch.class)) {Object moduleInstance = entryClass.newInstance();hookLoadPatch(new PatchCallback((IPatch) moduleInstance));}}} catch (Exception e) {return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);}return new PatchResult(true, PatchResult.NO_ERROR, "");}private static boolean isImplementInterface(Class<?> entry, Class<?> interClass) {Class<?>[] interfaces = entry.getInterfaces();if (interfaces == null) {return false;}for (int i = 0; i < interfaces.length; i++) {if (interfaces[i].equals(interClass)) {return true;}}return false;}/*** Get notified when a patch is loaded. This is especially useful to hook some patch-specific methods.*/private static void hookLoadPatch(PatchCallback callback) {synchronized (loadedPatchCallbacks) {loadedPatchCallbacks.add(callback);}}}
class PatchCallback {private final IPatch instance;protected PatchCallback(IPatch instance) {this.instance = instance;}protected static final PatchResult callAll(PatchParam param) {boolean isAllFailed = true;for (int i = 0; i < param.callbacks.length; i++) {try {((PatchCallback) param.callbacks[i]).call(param);isAllFailed = false;} catch (Throwable t) {t.printStackTrace();}}if (isAllFailed) {return new PatchResult(true, PatchResult.ALL_PATCH_FAILED, "All patch classes excute failed");} else {return new PatchResult(true, PatchResult.NO_ERROR, "");}}protected void call(PatchParam param) throws Throwable {if (param instanceof PatchParam)handlePatch((PatchParam) param);}protected void handlePatch(PatchParam lpparam) throws Throwable {instance.handlePatch(lpparam);}}
public interface IPatch {void handlePatch(PatchParam lpparam) throws Throwable;}
public static Unhook findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback) {if(parameterTypesAndCallback.length != 0 && parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof XC_MethodHook) {XC_MethodHook callback = (XC_MethodHook)parameterTypesAndCallback[parameterTypesAndCallback.length - 1];Method m = XposedHelpers.findMethodExact(clazz, methodName, parameterTypesAndCallback);Unhook unhook = hookMethod(m, callback);if(!(callback instanceof XC_MethodKeepHook) && !(callback instanceof XC_MethodKeepReplacement)) {ArrayList var6 = allUnhookCallbacks;synchronized(allUnhookCallbacks) {allUnhookCallbacks.add(unhook);}}return unhook;} else {throw new IllegalArgumentException("no callback defined");}}