[关闭]
@linux1s1s 2015-09-01T08:46:35.000000Z 字数 8548 阅读 3625

Android 热更新——非侵入AOP框架

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实现是完全非侵入式的,没有使用任何注解处理器,编织器或者字节码重写器。

Patch初步

首先从GitHub上拉下来代码有几个坑需要注意:

接下来我们看看具体的流程:

首先需要我们动态监测AOP环境

  1. runPatchApk();

这里需要注意的是PatchMain.load()这个方法,该方法的主要用途是加载patch APK的所有类,并将实现IPatch的类添加到List中去,然后通过匹配加载的类或者类方法来实现非侵入式AOP。

  1. public void runPatchApk() {
  2. if (android.os.Build.VERSION.SDK_INT == 21) {
  3. return;
  4. }
  5. if (!DexposedBridge.canDexposed(this)) {
  6. Log.d("Hotpatch", "This device doesn't support dexposed!");
  7. return;
  8. }
  9. File cacheDir = getExternalCacheDir();
  10. if (cacheDir != null) {
  11. String fullpath = cacheDir.getAbsolutePath() + File.separator + "PATCH_NAME.apk";
  12. PatchResult result = PatchMain.load(this, fullpath, null);
  13. if (result.isSuccess()) {
  14. Log.e("Hotpatch", "patch success!");
  15. } else {
  16. Log.e("Hotpatch", "patch error is " + result.getErrorInfo());
  17. }
  18. }
  19. }

DexposedBridge.canDexposed(this)

  1. public static synchronized boolean canDexposed(Context context) {
  2. return !DeviceCheck.isDeviceSupport(context)?false:loadDexposedLib(context);
  3. }
  4. private static boolean loadDexposedLib(Context context) {
  5. try {
  6. if(VERSION.SDK_INT != 10 && VERSION.SDK_INT != 9) {
  7. if(VERSION.SDK_INT > 19) {
  8. System.loadLibrary("dexposed_l");
  9. } else {
  10. System.loadLibrary("dexposed");
  11. }
  12. } else {
  13. System.loadLibrary("dexposed2.3");
  14. }
  15. return true;
  16. } catch (Throwable var2) {
  17. return false;
  18. }
  19. }

Patch实例

  1. public class Activity extends BaseSherlockSubActivity implements
  2. OnNewIconUIRefreshListener {
  3. private void showDialog() {
  4. final AlertDialog.Builder builder = new AlertDialog.Builder(this);
  5. builder.setTitle("Dexposed sample")
  6. .setMessage("Please clone patchsample project to generate apk, and copy it to \"/Android/data/PACKAGE_NAME/cache/PATCH_NAME.apk\"")
  7. .setPositiveButton("ok", new DialogInterface.OnClickListener() {
  8. public void onClick(DialogInterface dialog, int whichButton) {
  9. }
  10. }).create().show();
  11. }
  12. }

假如我们上线的代码中如上所示,在弹层中出现文案bug,那么该如何热更新。
代码修复操作在Patch工程中,添加如下代码:

  1. public class DialogPatch implements IPatch {
  2. @Override
  3. public void handlePatch(final PatchParam arg0) throws Throwable {
  4. Class<?> cls = null;
  5. try {
  6. cls = arg0.context.getClassLoader().loadClass("com.android.activity.Activity");
  7. } catch (ClassNotFoundException e) {
  8. e.printStackTrace();
  9. return;
  10. }
  11. DexposedBridge.findAndHookMethod(cls, "showDialog", new XC_MethodReplacement() {
  12. @Override
  13. protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
  14. final Activity mainActivity = (Activity) param.thisObject;
  15. AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
  16. builder.setTitle("Fanli Dexposed sample").setMessage("The dialog is shown from patch apk!").setPositiveButton("ok", new OnClickListener() {
  17. public void onClick(DialogInterface dialog, int whichButton) {
  18. Class<?> clsInner;
  19. try {
  20. clsInner = arg0.context.getClassLoader().loadClass("com.android.activity.OutObject");
  21. } catch (ClassNotFoundException e) {
  22. e.printStackTrace();
  23. return;
  24. }
  25. try {
  26. OutObject outObject = (OutObject) clsInner.newInstance();
  27. if (outObject.callFromOutMethod()) {
  28. AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
  29. builder.setTitle("Fanli Dexposed sample").setMessage("com.android.activity.OutObject is Worked!")
  30. .setPositiveButton("ok", new OnClickListener() {
  31. @Override
  32. public void onClick(DialogInterface dialog, int which) {
  33. dialog.dismiss();
  34. }
  35. }).create().show();
  36. }
  37. } catch (InstantiationException e) {
  38. e.printStackTrace();
  39. } catch (IllegalAccessException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. }).create().show();
  44. return null;
  45. }
  46. });
  47. }
  48. }

然后将这个patch APK 传到Server,在主APK中通过下载patch apk到指定目录,然后动态监测AOP环境并loadPatch即可实现热更新。
接下来如果应用到实际项目中需要完善的有以下几点:

  1. 动态监测AOP环境 (android server主动监测 patch包)
  2. 动态加载Patch文件 (更新pathc以后第一时间加载patch)

Patch测试:

基于以上实现方案测试的环境包括:
Dalvik 4.0-4.4均已经通过
目前 ART 5.0 以及以上版本 尚未通过。(待更新Native包和Jar包)

  1. 静态变量无法做补丁,因为静态变量初始化在补丁初始化之前
  2. 涉及到动态链接库的调用无法做补丁
  3. 补丁模块应用在Application初始化的时候,无法在app运行时热替换,只能等下次启动或切后台后启动
  4. 补丁方式是以Context类作为切入点替换,补丁中所有相关类的替换需要带上Context类
  5. Application类无法替换
  1. 静态变量可以做补丁,但是final型变量无法做补丁。
  2. 涉及到动态链接库的调用可以引入Jar,然后在做相应的补丁操作。
  3. 可以在运行时做热更新,时间精度自己控制。
  4. Context取Application Context,不需要其他Context。
  5. Application类可以通过替换其中的方法完成类的替换。

附录Patch

PatchMain

  1. public class PatchMain {
  2. private static final ReadWriteSet<PatchCallback> loadedPatchCallbacks = new ReadWriteSet<PatchCallback>();
  3. /**
  4. * Load a runnable patch apk.
  5. *
  6. * @param context the application or activity context.
  7. * @param apkPath the path of patch apk file.
  8. * @param contentMap the object maps that will be used by patch classes.
  9. * @return PatchResult include if success or error detail.
  10. */
  11. public static PatchResult load(Context context, String apkPath, HashMap<String, Object> contentMap) {
  12. if (!new File(apkPath).exists()) {
  13. return new PatchResult(false, PatchResult.FILE_NOT_FOUND, "FILE not found on " + apkPath);
  14. }
  15. PatchResult result = loadAllCallbacks(context, apkPath, context.getClassLoader());
  16. if (!result.isSuccess()) {
  17. return result;
  18. }
  19. if (loadedPatchCallbacks.getSize() == 0) {
  20. return new PatchResult(false, PatchResult.NO_PATCH_CLASS_HANDLE, "No patch class to be handle");
  21. }
  22. PatchParam lpparam = new PatchParam(loadedPatchCallbacks);
  23. lpparam.context = context;
  24. lpparam.contentMap = contentMap;
  25. return PatchCallback.callAll(lpparam);
  26. }
  27. private static PatchResult loadAllCallbacks(Context context, String apkPath, ClassLoader cl) {
  28. try {
  29. // String dexPath = new File(context.getFilesDir(), apkPath.).getAbsolutePath();
  30. File dexoptFile = new File(apkPath + "odex");
  31. if (dexoptFile.exists()) {
  32. dexoptFile.delete();
  33. }
  34. ClassLoader mcl = null;
  35. try {
  36. mcl = new DexClassLoader(apkPath, context.getFilesDir().getAbsolutePath(), null, cl);
  37. } catch (Throwable e) {
  38. return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);
  39. }
  40. DexFile dexFile = DexFile.loadDex(apkPath, context.getFilesDir().getAbsolutePath() + File.separator + "patch.odex", 0);
  41. Enumeration<String> entrys = dexFile.entries();
  42. // clean old callback
  43. synchronized (loadedPatchCallbacks) {
  44. loadedPatchCallbacks.clear();
  45. }
  46. while (entrys.hasMoreElements()) {
  47. String entry = entrys.nextElement();
  48. Class<?> entryClass = null;
  49. try {
  50. entryClass = mcl.loadClass(entry);
  51. } catch (ClassNotFoundException e) {
  52. e.printStackTrace();
  53. break;
  54. }catch (IllegalAccessError e) {
  55. e.printStackTrace();
  56. break;
  57. }
  58. if (isImplementInterface(entryClass, IPatch.class)) {
  59. Object moduleInstance = entryClass.newInstance();
  60. hookLoadPatch(new PatchCallback((IPatch) moduleInstance));
  61. }
  62. }
  63. } catch (Exception e) {
  64. return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);
  65. }
  66. return new PatchResult(true, PatchResult.NO_ERROR, "");
  67. }
  68. private static boolean isImplementInterface(Class<?> entry, Class<?> interClass) {
  69. Class<?>[] interfaces = entry.getInterfaces();
  70. if (interfaces == null) {
  71. return false;
  72. }
  73. for (int i = 0; i < interfaces.length; i++) {
  74. if (interfaces[i].equals(interClass)) {
  75. return true;
  76. }
  77. }
  78. return false;
  79. }
  80. /**
  81. * Get notified when a patch is loaded. This is especially useful to hook some patch-specific methods.
  82. */
  83. private static void hookLoadPatch(PatchCallback callback) {
  84. synchronized (loadedPatchCallbacks) {
  85. loadedPatchCallbacks.add(callback);
  86. }
  87. }
  88. }

PatchCallback

  1. class PatchCallback {
  2. private final IPatch instance;
  3. protected PatchCallback(IPatch instance) {
  4. this.instance = instance;
  5. }
  6. protected static final PatchResult callAll(PatchParam param) {
  7. boolean isAllFailed = true;
  8. for (int i = 0; i < param.callbacks.length; i++) {
  9. try {
  10. ((PatchCallback) param.callbacks[i]).call(param);
  11. isAllFailed = false;
  12. } catch (Throwable t) {
  13. t.printStackTrace();
  14. }
  15. }
  16. if (isAllFailed) {
  17. return new PatchResult(true, PatchResult.ALL_PATCH_FAILED, "All patch classes excute failed");
  18. } else {
  19. return new PatchResult(true, PatchResult.NO_ERROR, "");
  20. }
  21. }
  22. protected void call(PatchParam param) throws Throwable {
  23. if (param instanceof PatchParam)
  24. handlePatch((PatchParam) param);
  25. }
  26. protected void handlePatch(PatchParam lpparam) throws Throwable {
  27. instance.handlePatch(lpparam);
  28. }
  29. }

IPatch

  1. public interface IPatch {
  2. void handlePatch(PatchParam lpparam) throws Throwable;
  3. }

DexposedBridge

  1. public static Unhook findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback) {
  2. if(parameterTypesAndCallback.length != 0 && parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof XC_MethodHook) {
  3. XC_MethodHook callback = (XC_MethodHook)parameterTypesAndCallback[parameterTypesAndCallback.length - 1];
  4. Method m = XposedHelpers.findMethodExact(clazz, methodName, parameterTypesAndCallback);
  5. Unhook unhook = hookMethod(m, callback);
  6. if(!(callback instanceof XC_MethodKeepHook) && !(callback instanceof XC_MethodKeepReplacement)) {
  7. ArrayList var6 = allUnhookCallbacks;
  8. synchronized(allUnhookCallbacks) {
  9. allUnhookCallbacks.add(unhook);
  10. }
  11. }
  12. return unhook;
  13. } else {
  14. throw new IllegalArgumentException("no callback defined");
  15. }
  16. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注