热修复

热修复

装载请注明出处:https://blog.csdn.net/xiaowu_zhu/article/details/79792533

热修复,其实已经不是一个新技术了,目前发展的也有好几种方案了,奈何,以前我并没有使用过,只是做了大致的了解,最近看了某位大神的简书,然后自己动手实践了吧,分析了哈原理,再次分享哈。

在一次的版本发布后,突然发现某了某个小bug,或者优化了一些东西,我们不可能再次去发布新版本,频繁发布版本,用户更新成本大, 并且用户有可能不会立马去更新,所以才有了热修复的开发流程来代替传统的开发流程。

热修复开发流程

版本1.0上线 >> 用户安装 >> 发现bug >> 紧急修复 >> 打出补丁,推送给用户>> 自动拉取补丁修复

热修复的开发流程显得更加灵活,优势很多:

  • 无需重新发版,实时高效热修复
  • 用户无感知修复,无需下载新的应用,代价小
  • 修复成功率高,把损失降到最低

业界热门的修复技术

热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed,腾讯QQ空间的超级补丁技术和微信的Tinker。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复。具体的细节请看:www.infoq.com/cn/articles/Android-hot-fix

目前我使用过的方案有: 阿里的AndFix和基于ClassLoader的修复方案。

阿里的AndFix

源码:AndFix

简介

AndFix 是阿里的一套热修复方案,它目前只能修复方法。

这里写图片描述

Android 使用方式

  • gradle

    dependencies {
    compile 'com.alipay.euler:andfix:0.5.0@aar'
    }
  • Application中初始化PatchManger

    patchManager = new PatchManager(context);
    patchManager.init(appversion);//current version
  • 加载Patch

    patchManager.loadPatch();
  • 动态加载Patch

    String patchPath = Environment.getExternalStorageDirectory()
      + File.separator + "fix.apatch";
    File file = new File(patchPath);
    if (file.exists()) {
      try {
          BaseApplication.sPatchManager.addPatch(patchPath);
          Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
      } catch (IOException e) {
          e.printStackTrace();
          Toast.makeText(this, "修复失败", Toast.LENGTH_SHORT).show();
      }
    }

    注意:在本地加载时,一定要注意添加存储的读取权限和动态权限。

  • 代码混淆

    如果需要代码混淆,请在混淆规则中加入:

    -keep class * extends java.lang.annotation.Annotation
    -keepclasseswithmembernames class * {
      native <methods>;
    }

生成差分包

  • 去github上下载 差分包生成工具

  • 将两个apk拷贝到工具包中的目录下

  • 通过命令生成.apatch

    usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
    -f: 没有bug的新版本
    -t: 有bug的旧版本
    -o:生成差分包`.apatch`的位置
    -k: apk打包的签名文件
    -p:打包签名文件的密码
    -a:签名密钥的别名
    -e:签名密钥别名的密码

    例如:apkpatch.bat -f new.apk -t old.apk -o out -k joke.jks -p 123456 -a joke -e 123456

差分包的源码

通过命令生成的差分包,通过重命名为.zip,提取.dex,通过反编译工具,我们可以看到:

@MethodReplace(clazz="cn.xwj.androidfix.MainActivity", method="test")
public void test(View paramView){
    Toast.makeText(this, "test: 0", 1).show();
}

通过命令工具,比对出是哪个方法被修改了,然后就会添加一个@MethodReplace注解,然后通过调用native方法。在andfix.cpp中:

static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
                          jobject dest) {
    if (isArt) {
        art_replaceMethod(env, src, dest);
    } else {
        dalvik_replaceMethod(env, src, dest);
    }
}

4.4 的替换方法,可以看到通过底层替换了修改的方法的指向。

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
    jobject clazz = env->CallObjectMethod(dest, jClassMethod);
    ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
            dvmThreadSelf_fnPtr(), clazz);
    clz->status = CLASS_INITIALIZED;

    Method* meth = (Method*) env->FromReflectedMethod(src);
    Method* target = (Method*) env->FromReflectedMethod(dest);
    LOGD("dalvikMethod: %s", meth->name);

//  meth->clazz = target->clazz;
    meth->accessFlags |= ACC_PUBLIC;
    meth->methodIndex = target->methodIndex;
    meth->jniArgInfo = target->jniArgInfo;
    meth->registersSize = target->registersSize;
    meth->outsSize = target->outsSize;
    meth->insSize = target->insSize;

    meth->prototype = target->prototype;
    meth->insns = target->insns;
    meth->nativeFunc = target->nativeFunc;
}

开发中的注意事项

  1. 每次生成之后一定要测试;
  2. 尽量的不要分包,不要分多个dex
  3. 混淆的时候,设计到NDK AndFix.java 不要混淆
  4. 生成包之后一般会加固什么的,这个时候生成的差分包,一定要在之前去生成。
  5. 既然是去修复方法,第一个不能增加成员变量,不能增加方法
  6. 如果下载到本地去添加的话,一定要注意添加权限和6.0之后动态生成权限

基于ClassLoader方式(Tinker)

Activity加载流程

  • Activity
@Override
public void startActivity(Intent intent) {
    this.startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
    if (options != null) {
        startActivityForResult(intent, -1, options);
    } else {
        // Note we want to go through this call for compatibility with
        // applications that may have overridden the method.
        startActivityForResult(intent, -1);
    }
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
                                   @Nullable Bundle options) {
    if (mParent == null) {
        options = transferSpringboardActivityOptions(options);
        //Instrumentation#execStartActivity
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, this,
            intent, requestCode, options);
        .....
    }
    ......
}
  • Instrumentation
public ActivityResult execStartActivity(
    Context who, IBinder contextThread, IBinder token, Activity target,
    Intent intent, int requestCode, Bundle options) {
    .......
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        // ActivityManagerService#startActivity
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                           intent.resolveTypeIfNeeded(who.getContentResolver()),
                           token, target != null ? target.mEmbeddedID : null,
                           requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}
public static IActivityManager getService() {
    return IActivityManagerSingleton.get(); //得到的就是ActivityManager
}
//单例
private static final Singleton<IActivityManager> IActivityManagerSingleton =
    new Singleton<IActivityManager>() {
    @Override
    protected IActivityManager create() {
        final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
        final IActivityManager am = IActivityManager.Stub.asInterface(b);
        return am;
    }
};

然后会调用IBinder底层方法,进行Activity的启动,这里就不做太深的描述了,以后谈到Activity的启动流程的时候,通过源码,再详细看看。

通过IBinder底层方法后,会调用ActivityThread方法来启动Activity

  • ActivityThread

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
      // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
     .......
      Activity activity = null;
      try {
          //通过ClassLoader加载Activity
          java.lang.ClassLoader cl = appContext.getClassLoader();
          //newActivity
          activity = mInstrumentation.newActivity(
              cl, component.getClassName(), r.intent);
          StrictMode.incrementExpectedActivityCount(activity.getClass());
          r.intent.setExtrasClassLoader(cl);
          r.intent.prepareToEnterProcess();
          if (r.state != null) {
              r.state.setClassLoader(cl);
          }
      } catch (Exception e) {
          if (!mInstrumentation.onException(activity, e)) {
              throw new RuntimeException(
                  "Unable to instantiate activity " + component
                  + ": " + e.toString(), e);
          }
      }
      .....
      return activity;
    }
  • Instrumentation

    public Activity newActivity(ClassLoader cl, String className,Intent intent)
      throws InstantiationException, IllegalAccessException,ClassNotFoundException {
      //通过classLoader加载class
      return (Activity)cl.loadClass(className).newInstance();
    }

    通过寻找appContext.getClassLoader()方法,我们可以知道,系统会去创建一个系统的ClassLoader

    @Override
    public ClassLoader getClassLoader() {
      //不管是mClassLoader,mPackageInfo.getClassLoader,最后调用的都是ClassLoader.getSystemCloader()获取的classLoader一样
      return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
    }
  • ClassLoader

    private static ClassLoader createSystemClassLoader() {
      String classPath = System.getProperty("java.class.path", ".");
      String librarySearchPath = System.getProperty("java.library.path", "");
    //PathClassLoader
      return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

    从上面的代码可以看出,ClassLoader.getSystemClassLoader()创建的是一个PathClassLoader实例。

  • PathClassLoader

    PathClassLoader继承了BaseDexClassLoader

    public class PathClassLoader extends BaseDexClassLoader {
      public PathClassLoader(String dexPath, ClassLoader parent) {
          super((String)null, (File)null, (String)null, (ClassLoader)null);
          throw new RuntimeException("Stub!");
      }
    
      public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
          super((String)null, (File)null, (String)null, (ClassLoader)null);
          throw new RuntimeException("Stub!");
      }
    }
  • BaseDexClassLoader

    public class BaseDexClassLoader extends ClassLoader {
      public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
          throw new RuntimeException("Stub!");
      }
    
      //这个就是我们需要的findClass
      protected Class<?> findClass(String name) throws ClassNotFoundException {
          throw new RuntimeException("Stub!");
      }
      ....
    }

    由于BaseDexClassLoader在我们下载的Android SDK是隐藏的,所以我们需要去查看没有阉割版的源码。在这里我提供两个地址:

    源码查看网站 Android 8.0源码

    通过源码,我们可以看到:

    public class BaseDexClassLoader extends ClassLoader {
      /* @NonNull */ private static volatile Reporter reporter = null;
    
      private final DexPathList pathList;
    
      public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                                String librarySearchPath, ClassLoader parent) {
          super(parent);
          this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    
          if (reporter != null) {
              reporter.report(this.pathList.getDexPaths());
          }
      }
    
      public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
          // TODO We should support giving this a library search path maybe.
          super(parent);
          this.pathList = new DexPathList(this, dexFiles);
      }
    
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
          List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
          Class c = pathList.findClass(name, suppressedExceptions);
          if (c == null) {
              ClassNotFoundException cnfe = new ClassNotFoundException(
                  "Didn't find class \"" + name + "\" on path: " + pathList);
              for (Throwable t : suppressedExceptions) {
                  cnfe.addSuppressed(t);
              }
              throw cnfe;
          }
          return c;
      }
    }

    BaseDexClassLoader调用的findClass()中调用了DexPathList中的findClass()

  • DexPathList

    final class DexPathList {
      private Element[] dexElements;
      public Class<?> findClass(String name, List<Throwable> suppressed) {
          for (Element element : dexElements) {
              Class<?> clazz = element.findClass(name, definingContext, suppressed);
              if (clazz != null) { //遍历dexElements,找到就立马返回
                  return clazz;
              }
          }
    
          if (dexElementsSuppressedExceptions != null) {
              suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
          }
          return null;
      }
    }

Activity加载核心

  • 类:PathDexClassLoader —> BaseDexClassLoader —> ClassLoader
  • 方法: BaseDexClassLoader.findClass() –> DexPathList.findClass()—> 遍历dexElements

自定义实现

public void addFixDex(@NonNull String dexPath) throws Exception {
    //判断路径是否为空
    if (TextUtils.isEmpty(dexPath)) {
        throw new IllegalArgumentException("dexPath is null");
    }
    //判断文件是否存在
    File src = new File(dexPath);
    if (!src.exists()) {
        throw new FileNotFoundException(dexPath);
    }

    File dest = new File(mDexRootDir, src.getName());
    //判断文件是否已经被加载
    if (dest.exists()) {
        Log.d(TAG, "dex [" + dexPath + "] has already loaded");
        return;
    }
    //将文件拷贝到指定的目录
    FileUtils.copyFile(src, dest);
    //获取app的classLoader
    ClassLoader appClassLoader = mContext.getClassLoader();
    //获取BaseDexClassLoader中的pathList 字段
    Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
    pathListField.setAccessible(true); //设置可访问 
    Object pathList = pathListField.get(appClassLoader); //得到pathList的值
    //通过反射获取DexPathList中的dexElements字段
    Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
    dexElementsField.setAccessible(true); //设置可访问
    Object dexElements = dexElementsField.get(pathList);//得到dexElements

    //创建dex的解压目录
    File optimizedDirectory = new File(mDexRootDir + File.separator + "dex");
    if (!optimizedDirectory.exists()) {//不存在需要创建
        optimizedDirectory.mkdirs(); //创建
    }
    //创建一个BaseDexClassLoader
    BaseDexClassLoader classLoader = new BaseDexClassLoader(
        dest.getAbsolutePath(), //dex的路径
        optimizedDirectory, 
        null, //library的解压目录
        appClassLoader //app的classLoader
    );
    //通过classLoader获取fixDex中的pathList的值
    Object fixDexPathList = pathListField.get(classLoader);
    //通过fixDexPathList获取PathDexList中的dexElements中的值
    Object fixDexElements = dexElementsField.get(fixDexPathList);
    //和并dexElements
    Object object = combineDexElements(dexElements, fixDexElements);
    //将合并的重新设置给appClassLoader所在的dexElements中
    dexElementsField.set(pathList, object);
    Log.d(TAG, "完成");
}

上面只是一个初略的实现。具体代码:androidFix

个人博客:https://xiaowujiang.cn 欢迎访问。

posted @ 2018-04-02 17:42  jxiaow  阅读(65)  评论(0编辑  收藏  举报