Android 热修补方案(AndFix)

AndFix介绍

AndFix是一个Android App的在线热补丁框架。使用此框架,我们能够在不重复发版的情况下,在线修改App中的Bug。AndFix就是 “Android Hot-Fix”的缩写。

AndFix支持Android 2.3到6.0版本,并且支持arm 与 X86系统架构的设备。完美支持Dalvik与ART的Runtime。

AndFix 的补丁文件是以 .apatch 结尾的文件。

AndFix是阿里的开源项目。https://github.com/alibaba/AndFix

小例子:

下载demo APK http://120.55.185.35:8080/old.apk
demo 下载地址,或者扫描下面二维码,安装好apk后运行AndFixDemo,点击“提示信息”按钮,跳出“未修复的toast”,点击“开始修复”,app会到远程服务端下载补丁包更新,大概会持续几秒钟,等待几秒后再次点击“提示信息”按钮,会弹出修复好的内容

实际运行中则不需要点击“开始修复”按钮,在demo中为了对比效果,所以加了按钮去控制
实际运行中检测需要打补丁的方案:
  1. 类似检测升级,在打开app或者某个页面去检测
  2. 服务端推送(需要和服务端定义更多实现细节)

 

AndFix的优点很明显
  1. 补丁包很小(上面的例子,补丁包才几K),打补丁的速度很快
  2. 打好补丁后,后续都不用再打了

使用方法:

1. 添加依赖

Android 热修补方案(AndFix)_第1张图片
 

2. 在自定义Application中初始化,并在AndroidManifest.xml中注册该Application

Android 热修补方案(AndFix)_第2张图片
 

3.在activity 中打补丁,(这里省略了检测步骤,默认就是直接下载补丁并更新,实际开发中需要对补丁的版本进行检测,更新好后删除补丁包,动态获取补丁路径等等)

Android 热修补方案(AndFix)_第3张图片
 

4.到这里已经完成了配置工作,接下来用正式的key的打包,就生成了old.apk, 把old.apk放到了服务器上,项目就上线了

 

5.项目上线后,会有各种突发情况,(比如文案修改,紧急bug,造成app crash等麻烦,正常情况只有进行版本升级),这里看下AndFix热修补的步骤,以修改showToast方法为例子


 

6.现在把showToast方法修改,并打包,命名为fix.apk

Android 热修补方案(AndFix)_第4张图片
 

7.下载apkpatch工具

Android 热修补方案(AndFix)_第5张图片
 

8.制作补丁包,cd 到目录下,运行apkpatch.bat -f fix.apk -t old.apk -o output1 -k demo.jks -p 123456 -a demo -e 123456

(这里-t 为老的apk -f为修复过的apk,-o 为输出目录 -k 为打包的key -p -e 为密码 -a 为别名)
Android 热修补方案(AndFix)_第6张图片
 

9.屏幕输出了增加了修改了toast的方法 ,同时目录下新增了output1 目录,点进去查看,其中.apatch为真正的补丁包


 

10. 用dex2Jar工具把diff文件转成jar文件,在用jd.gui查看

Android 热修补方案(AndFix)_第7张图片
其实这个工具就是比对两个dex文件,分析出修改过的地方,然后生成补丁包
 

11. 把.apatch命名为app.apatch上传至服务器,坐等客户端打补丁


 

源码分析:

1首先来看下在application中的初始化

@Override
    public void onCreate() {
        super.onCreate();
        // 初始化patch管理类
        mPatchManager = new PatchManager(this);
        // 初始化patch版本
        mPatchManager.init("2.0");
        // 加载已经添加到PatchManager中的patch
        mPatchManager.loadPatch();
    }

 

2 构造了PatchManager对象,来看下代码

public PatchManager(Context context) {
        mContext = context;
        mAndFixManager = new AndFixManager(mContext); //初始化AndFixManager,等会再介绍 
        //getFileDir 获取的是/data/data/<application package>/files
        //在这里是 /data/data/<application package>/files/apatch
        mPatchDir = new File(mContext.getFilesDir(), DIR);
        // 支持并发访问的有序的补丁集合
        mPatchs = new ConcurrentSkipListSet<Patch>();
        // ClassLoader的集合,同样也是基于线程安全的
        mLoaders = new ConcurrentHashMap<String, ClassLoader>();
    }

 

3 初始化patch版本,代码

public void init(String appVersion) {
        //再次检测patch存放路径
        if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
            Log.e(TAG, "patch dir create error.");
            return;
        } else if (!mPatchDir.isDirectory()) {// not directory
            mPatchDir.delete();
            return;
        }
        //用SharedPreferences 获取path的版本
        SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
                Context.MODE_PRIVATE);
        String ver = sp.getString(SP_VERSION, null);
        if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
        //如果是第一次使用或者版本不一致,则删除所有Patch ,这里equalsIgnoreCase是忽略大小写的equals
            cleanPatch();
        // 存放版本 
            sp.edit().putString(SP_VERSION, appVersion).commit();
        } else {
        // 加载有所的Patch
            initPatchs();
        }
    }

    private void initPatchs() {
        File[] files = mPatchDir.listFiles();
        for (File file : files) {
            addPatch(file);
        }
    }

 

4 加载已经添加到PatchManager中的patch,代码

/** * load patch,call when application start * 这里写的也很清楚了,在程序启动的时候调用 */
    public void loadPatch() {
        //首先加载了通用的类加载器
        mLoaders.put("*", mContext.getClassLoader());// wildcard
        Set<String> patchNames;
        List<String> classes;
        //遍历每个补丁包 Patch 的结构HashMap<String, List<String>>()
        //实际中,只会有一个key,就是你修改后apk的名字,list中存放修改的className
        //fix----[cv.cocoa.com.andfixdemo.MainActivity_CF]
        for (Patch patch : mPatchs) {
            patchNames = patch.getPatchNames();
            for (String patchName : patchNames) {
                classes = patch.getClasses(patchName);
                //更新补丁
                mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                        classes);
            }
        }
    }

 

5.在Application中的初始化代码就完成了,其中还有一个AndFixManager没讲,AndFixManager在构造前,会做一个兼容性的检测,放在了Compat 类中,代码

public class Compat {
    public static boolean isChecked = false;
    public static boolean isSupport = false;

    /** * whether support on the device * 需要对兼容性进行检测,检测的判断是不能是YunOs的手机,sdk的版本必须是在2.3-6.0之间 * @return true if the device support AndFix */
    public static synchronized boolean isSupport() {
        if (isChecked)
            return isSupport;

        isChecked = true;
        // AndFix.setup()判断是Dalvik还是Art虚拟机,来注册Native方法
        if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {
            isSupport = true;
        }

        if (inBlackList()) {
            isSupport = false;
        }

        return isSupport;
    }
}
/** * initialize * * @return true if initialize success * * 判断是Dalvik还是Art虚拟机,并初始化native的方法, * * 官方文档中说到https://developer.android.com/guide/practices/verifying-apps-art.html * You can verify which runtime is in use by calling System.getProperty("java.vm.version"). If ART is in use, the property's value is "2.0.0" or higher. * */
    public static boolean setup() {
        try {
            final String vmVersion = System.getProperty("java.vm.version");
            boolean isArt = vmVersion != null && vmVersion.startsWith("2");
            int apilevel = Build.VERSION.SDK_INT;
            return setup(isArt, apilevel);
        } catch (Exception e) {
            Log.e(TAG, "setup", e);
            return false;
        }
    }

6.然后再来看下AndFixManager 的代码

//最关键的就是这里,获取到Class对象后,用反射获取修改的方法
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
        //java反射获取Class的方法
        Method[] methods = clazz.getDeclaredMethods();
        MethodReplace methodReplace;
        String clz;
        String meth;
        for (Method method : methods) {
            // 找到MethodReplace的注解
            methodReplace = method.getAnnotation(MethodReplace.class);
            if (methodReplace == null)
                continue;
            //对照上面的demo,clz就是cv.cocoa.com.andfixdemo.MainActivity
            clz = methodReplace.clazz();  
            //对照上面的demo,meth 就是showToast
            meth = methodReplace.method();
            if (!isEmpty(clz) && !isEmpty(meth)) {
                //替换方法
                replaceMethod(classLoader, clz, meth, method);
            }
        }
    }

    /** * replace method * * @param classLoader classloader * @param clz class * @param meth name of target method * @param method source method */
    private void replaceMethod(ClassLoader classLoader, String clz,
            String meth, Method method) {
        try {
            String key = clz + "@" + classLoader.toString();
            Class<?> clazz = mFixedClass.get(key);
            if (clazz == null) {// class not load
                Class<?> clzz = classLoader.loadClass(clz);
                // initialize target class
                clazz = AndFix.initTargetClass(clzz);
            }
            if (clazz != null) {// initialize class OK
                mFixedClass.put(key, clazz);
                Method src = clazz.getDeclaredMethod(meth,
                        method.getParameterTypes());
                //调用jni的方法
                AndFix.addReplaceMethod(src, method);
            }
        } catch (Exception e) {
            Log.e(TAG, "replaceMethod", e);
        }
    }

你可能感兴趣的:(Android 热修补方案(AndFix))