热补丁方案研究

What--什么是Hotfix[编辑]

HotFix是针对某一个具体的系统漏洞或安全问题而发布的专门解决该漏洞或安全问题的小程序,通常称为修补程序。

Why--为什么我们要用Hotfix[编辑]

   当Android发布App之后,如果突然发现了一个严重bug,而这个bug需要进行紧急修复。这时候我们通常的处理流程是:解决bug、重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。用户体验来很是糟糕。有没有办法不重新发布App,不需要用户重新下载覆盖安装,就可以完成Bug的修复?HotFix就是做这个事情的。网上已有各种Hotfix框架,主要有:AndFix、dexposed 、Xpose、Nuwa、HotFix。这里要介绍的是阿里巴巴开源的AndFix方案。Github地址:https://github.com/alibaba/AndFix

    AndFix,全称是Android hot-fix。是阿里开源的一个Android热补丁框架,允许APP在不重新发布版本的情况下修复线上的bug。支持Android 2.3 到 6.0。

 

How--怎么用[编辑]

添加依赖[编辑]

首先添加依赖
compile 'com.alipay.euler:andfix:0.3.1@aar'

 

初始化[编辑]

然后在Application.onCreate() 中添加以下代码

patchManager = new PatchManager(context);

patchManager.init(appversion);//current version

patchManager.loadPatch();

 

可以用这句话获取appversion:
String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;


   注意每次appversion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。然后在需要的地方调用PatchManager的addPatch方法加载新补丁,比如可以在下载补丁文件之后调用。

打补丁[编辑]

   之后就是打补丁的过程了,首先生成一个apk文件,然后更改代码,在修复bug后生成另一个apk。通过官方提供的工具apkpatch生成一个.apatch格式的补丁文件,需要提供原apk,修复后的apk,以及一个签名文件。可以直接使用命令apkpatch查看具体的使用方法。
使用示例:
apkpatch -f LiumiAppProduct_v2.0.0_release.apk -t LiumiAppProduct_v2.0.0_release0.apk -o output2 -k mime.jks -p 123456 -a mime -e 123456

RTENOTITLE

  通过网络传输或者adb push的方式将apatch文件传到手机上,然后运行到addPatch的时候就会加载补丁。加载过的补丁会被保存到data/packagename/files/apatch_opt目录下,所以下载过来的补丁用过一次就可以删除了。

大致原理[编辑]

  apkpatch将两个apk做一次对比,然后找出不同的部分。可以看到生成的apatch了文件,后缀改成zip再解压开,里面有一个dex文件。通过jadx查看一下源码,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且变动的方法都被加上了一个叫@MethodReplace的annotation,通过clazz和method指定了需要替换的方法。
然后客户端sdk得到补丁文件后就会根据annotation来寻找需要替换的方法。最后由JNI层完成方法的替换。

多次打补丁[编辑]

  如果本地保存了多个补丁,那么AndFix会按照补丁生成的时间顺序加载补丁。具体是根据.apatch文件中的PATCH.MF的字段Created-Time。

安全性[编辑]

  readme提示开发者需要验证下载过来的apatch文件的签名是否就是在使用apkpatch工具时使用的签名,如果不验证那么任何人都可以制作自己的apatch文件来对你的APP进行修改。
但是我看到AndFix已经做了验证,如果补丁文件的证书和当前apk的证书不是同一个的话,就不能加载补丁。官网还有一条,提示需要验证optimize file的指纹,应该是为了防止有人替换掉本地保存的补丁文件,所以要验证MD5码,然而SecurityChecker类里面也已经做了这个工作。。但是这个MD5码是保存在sharedpreference里面,如果手机已经root那么还是可以被访问的。

混淆

-printmapping proguard.map
首先需要生成mapping文件记录混淆规则,之后可以把printmapping 这句话注释掉,每次只使用applymapping。
-applymapping proguard.map
然后在下面加上

-keep class * extends java.lang.annotation.Annotation-keepclasseswithmembernames class * {

    native <methods>;

}

-keep class com.alipay.euler.andfix.** { *; }

碰到的问题[编辑]

刚开始做的demo中,每次产生的apatch文件用的名字都是相同的,结果导致只有第一次的补丁能生效。
看了源码后发现只有每次名字不同才能加载,log中应该也有提示,但是没注意到。

File src = new File(path);

File dest = new File(mPatchDir, src.getName());i

f(!src.exists()){

    throw new FileNotFoundException(path);

}

if (dest.exists()) {

    Log.d(TAG, "patch [" + path + "] has be loaded.");

    return;

}

局限性[编辑]

不支持YunOS

无法添加新类和新的字段

需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露。

使用加固平台可能会使热补丁功能失效(看到有人在360加固提了这个问题,自己还未验证)。

与Nuwa对比[编辑]

Nuwa是另一个热补丁框架,原理是基于QQ空间团队提出的安卓App热补丁动态修复技术介绍
与Nuwa相比,AndFix有一下优点:

  1、不需要重启APP即可应用补丁。

  2、安全性更好,Nuwa后面的版本应该也会加上安全方面的内容。

但是也有缺点:无法添加类和字段

 

原理及源码解析[编辑]

 

AndFix热补丁原理就是在native动态替换方法java层的代码,通过native层hook java层的代码。 
RTENOTITLE

注:在Native层使用指针替换的方式替换bug方法,已达到修复bug的目的。

使用AndFix修复热修复的整体流程:

RTENOTITLE

方法替换过程:

RTENOTITLE

源码解析

解析源码从使用的方法一一解析。

在自定义Application中初始化PatchManger:

PatchManager mPatchManager = new PatchManager();

 

直接实例化了一个PatchManger实例对象,接下看PatchManager类源码:

public PatchManager(Context context) {

        mContext = context;

        mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager

        mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch补丁文件的文件夹

        mPatchs = new ConcurrentSkipListSet<Patch>();//初始化存在Patch类的集合,此类适合大并发

        mLoaders = new ConcurrentHashMap<String, ClassLoader>();//初始化存放类对应的类加载器集合

}

然后看AndFixManager的初始化:

public AndFixManager(Context context) {

        mContext = context;

        mSupport = Compat.isSupport();//判断Android机型是否适支持AndFix

        if (mSupport) {

            mSecurityChecker = new SecurityChecker(mContext);//初始化签名判断类

            mOptDir = new File(mContext.getFilesDir(), DIR);//初始化patch文件存放的文件夹

            if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail

                mSupport = false;

                Log.e(TAG, "opt dir create error.");

            } else if (!mOptDir.isDirectory()) {// not directory

                mOptDir.delete();//如果不是文件目录就删除

                mSupport = false;

            }

        }

}

public static synchronized boolean isSupport() {//此处加了同步锁机制

        if (isChecked)

            return isSupport;

 

        isChecked = true;

        // not support alibaba's YunOs

        boolean isYunOs = isYunOS();//判断系统是否是YunOs系统,YunOs系统是阿里巴巴的系统

        boolean setup  =AndFix.setup();//判断是Dalvik还是Art虚拟机,来注册Native方法

        boolean isSupportSDKVersion = isSupportSDKVersion();//根据sdk版本判断是否支持

        if (!isYunOs && setup && isSupportSDKVersion) {//根据上面三个boolean值判断是否支持

            isSupport = true;

        }

 

        if (inBlackList()) {

            isSupport = false;

        }

        return isSupport;

}

private static boolean isSupportSDKVersion() {

        if (android.os.Build.VERSION.SDK_INT >= 8

                && android.os.Build.VERSION.SDK_INT <= 23) {

            return true;

        }

        return false;

}

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;

        }

}

//签名机制的初始化过程

public SecurityChecker(Context context) {

        mContext = context;

        init(mContext);

}

//主要是获取当前应用的签名及其他信息,为了判断与patch文件的签名是否一致

private void init(Context context) {

        try {

            PackageManager pm = context.getPackageManager();

            String packageName = context.getPackageName();

 

            PackageInfo packageInfo = pm.getPackageInfo(packageName,

                    PackageManager.GET_SIGNATURES);

            CertificateFactory certFactory = CertificateFactory

                    .getInstance("X.509");

            ByteArrayInputStream stream = new ByteArrayInputStream(

                    packageInfo.signatures[0].toByteArray());

            X509Certificate cert = (X509Certificate) certFactory

                    .generateCertificate(stream);

            mDebuggable = cert.getSubjectX500Principal().equals(DEBUG_DN);

            mPublicKey = cert.getPublicKey();

        } catch (NameNotFoundException e) {

            Log.e(TAG, "init", e);

        } catch (CertificateException e) {

            Log.e(TAG, "init", e);

    }

}

接下,看一下版本的初始化:

mPatchManager.init("version")

 

init方法源码:

public void init(String appVersion) {

        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 sp = mContext.getSharedPreferences(SP_NAME,

                Context.MODE_PRIVATE);//存储关于patch文件的信息

        //根据你传入的版本号和之前的对比,做不同的处理

        String ver = sp.getString(SP_VERSION, null);

        if (ver == null || !ver.equalsIgnoreCase(appVersion)) {

            cleanPatch();//删除本地patch文件

            sp.edit().putString(SP_VERSION, appVersion).commit();//并把传入的版本号保存

        } else {

            initPatchs();//初始化patch列表,把本地的patch文件加载到内存

    }

}

private void cleanPatch() {

        File[] files = mPatchDir.listFiles();

        for (File file : files) {

            mAndFixManager.removeOptFile(file);//删除所有的本地缓存patch文件

            if (!FileUtil.deleteFile(file)) {

                Log.e(TAG, file.getName() + " delete error.");

        }

    }

}

private void initPatchs() {

        File[] files = mPatchDir.listFiles();

        for (File file : files) {

            addPatch(file);//加载Patch文件

        }

}

private Patch addPatch(File file) {

        Patch patch = null;

        if (file.getName().endsWith(SUFFIX)) {

            try {

                patch = new Patch(file);//实例化Patch对象

                mPatchs.add(patch);//把patch实例存储到内存的集合中,在PatchManager实例化集合

            } catch (IOException e) {

                Log.e(TAG, "addPatch", e);

            }

        }

        return patch;

}

Patch文件的加载

public Patch(File file) throws IOException {

        mFile = file;

        init();

}

@SuppressWarnings("deprecation")private void init() throws IOException {

        JarFile jarFile = null;

        InputStream inputStream = null;

        try {

            jarFile = new JarFile(mFile);//使用JarFile读取Patch文件

            JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//获取META-INF/PATCH.MF文件

            inputStream = jarFile.getInputStream(entry);

            Manifest manifest = new Manifest(inputStream);

            Attributes main = manifest.getMainAttributes();

            mName = main.getValue(PATCH_NAME);//获取PATCH.MF属性Patch-Name

            mTime = new Date(main.getValue(CREATED_TIME));//获取PATCH.MF属性Created-Time

 

            mClassesMap = new HashMap<String, List<String>>();

            Attributes.Name attrName;

            String name;

            List<String> strings;

            for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {

                attrName = (Attributes.Name) it.next();

                name = attrName.toString();

                //判断name的后缀是否是-Classes,并把name对应的值加入到集合中,对应的值就是class类名的列表

                if (name.endsWith(CLASSES)) {

                    strings = Arrays.asList(main.getValue(attrName).split(","));

                    if (name.equalsIgnoreCase(PATCH_CLASSES)) {

                        mClassesMap.put(mName, strings);

                    } else {

                        mClassesMap.put(

                                name.trim().substring(0, name.length() - 8),// remove

                                                                            // "-Classes"

                                strings);

                    }

                }

            }

        } finally {

            if (jarFile != null) {

                jarFile.close();

            }

            if (inputStream != null) {

                inputStream.close();

            }

        }

 

}

loadPatch源码:

public void loadPatch() {

        mLoaders.put("*", mContext.getClassLoader());// wildcard

        Set<String> patchNames;

        List<String> classes;

        for (Patch patch : mPatchs) {

            patchNames = patch.getPatchNames();

            for (String patchName : patchNames) {

                classes = patch.getClasses(patchName);//获取patch对用的class类的集合List

                mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),

                        classes);//修复bug方法

            }

    }

}

fix bug:

public synchronized void fix(File file, ClassLoader classLoader,

            List<String> classes) {

        if (!mSupport) {

            return;

        }

        //判断patch文件的签名

        if (!mSecurityChecker.verifyApk(file)) {// security check fail

            return;

        }

 

        try {

            File optfile = new File(mOptDir, file.getName());

            boolean saveFingerprint = true;

            if (optfile.exists()) {

                // need to verify fingerprint when the optimize file exist,

                // prevent someone attack on jailbreak device with

                // Vulnerability-Parasyte.

                // btw:exaggerated android Vulnerability-Parasyte

                // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html

                if (mSecurityChecker.verifyOpt(optfile)) {

                    saveFingerprint = false;

                } else if (!optfile.delete()) {

                    return;

                }

            }

            //加载patch文件中的dex

            final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),

                    optfile.getAbsolutePath(), Context.MODE_PRIVATE);

 

            if (saveFingerprint) {

                mSecurityChecker.saveOptSig(optfile);

            }

 

            ClassLoader patchClassLoader = new ClassLoader(classLoader) {

                @Override

                protected Class<?> findClass(String className)

                        throws ClassNotFoundException {//重写ClasLoader的findClass方法

                    Class<?> clazz = dexFile.loadClass(className, this);

                    if (clazz == null

                            && className.startsWith("com.alipay.euler.andfix")) {

                        return Class.forName(className);// annotation’s class

                                                        // not found

                    }

                    if (clazz == null) {

                        throw new ClassNotFoundException(className);

                    }

                    return clazz;

                }

            };

            Enumeration<String> entrys = dexFile.entries();

            Class<?> clazz = null;

            while (entrys.hasMoreElements()) {

                String entry = entrys.nextElement();

                if (classes != null && !classes.contains(entry)) {

                    continue;// skip, not need fix

                }

                clazz = dexFile.loadClass(entry, patchClassLoader);//获取有bug的类文件

                if (clazz != null) {

                    fixClass(clazz, classLoader);// next code

                }

            }

        } catch (IOException e) {

            Log.e(TAG, "pacth", e);

    }

}

private void fixClass(Class<?> clazz, ClassLoader classLoader) {

        Method[] methods = clazz.getDeclaredMethods();

        MethodReplace methodReplace;

        String clz;

        String meth;

        for (Method method : methods) {

            //获取此方法的注解,因为有bug的方法在生成的patch的类中的方法都是有注解的,下面会给图进行展示

            methodReplace = method.getAnnotation(MethodReplace.class);

            if (methodReplace == null)

                continue;

            clz = methodReplace.clazz();//获取注解中clazz的值

            meth = methodReplace.method();//获取注解中method的值

            if (!isEmpty(clz) && !isEmpty(meth)) {

                replaceMethod(classLoader, clz, meth, method);//next code

            }

    }

}

private void replaceMethod(ClassLoader classLoader, String clz,

            String meth, Method method) {

        try {

            String key = clz + "@" + classLoader.toString();

            Class<?> clazz = mFixedClass.get(key);//判断此类是否被fix

            if (clazz == null) {// class not load

                Class<?> clzz = classLoader.loadClass(clz);

                // initialize target class

                clazz = AndFix.initTargetClass(clzz);//初始化class

            }

            if (clazz != null) {// initialize class OK

                mFixedClass.put(key, clazz);

                Method src = clazz.getDeclaredMethod(meth,

                        method.getParameterTypes());//根据反射获取到有bug的类的方法(有bug的apk)

                AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是补丁方法

            }

        } catch (Exception e) {

            Log.e(TAG, "replaceMethod", e);

    }

}

public static void addReplaceMethod(Method src, Method dest) {

        try {

            replaceMethod(src, dest);//调用了native方法,next code

            initFields(dest.getDeclaringClass());

        } catch (Throwable e) {

            Log.e(TAG, "addReplaceMethod", e);

        }

}

private static native void replaceMethod(Method dest, Method src);

由于Android4.4后才用的Art虚拟机,之前的系统都是Dalvik虚拟机,因此Native层写了2个方法,对不同的系统做不同的处理方式。

extern void dalvik_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Dalvikextern

void art_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Art

 

Dalvik replaceMethod的实现:

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->jniArgInfo = 0x80000000;

    meth->accessFlags |= ACC_NATIVE;//把Method的属性设置成Native方法

 

    int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);

    if (!dvmIsStaticMethod(meth))

        argsSize++;

    meth->registersSize = meth->insSize = argsSize;

    meth->insns = (void*) target;

 

    meth->nativeFunc = dalvik_dispatcher;//把方法的实现替换成native方法

}

Art replaceMethod的实现:

//不同的art系统版本不同处理也不同

extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(

        JNIEnv* env, jobject src, jobject dest) {

    if (apilevel > 22) {

        replace_6_0(env, src, dest);

    } else if (apilevel > 21) {

        replace_5_1(env, src, dest);

    } else {

        replace_5_0(env, src, dest);

    }

}

//以5.0为例:

void replace_5_0(JNIEnv* env, jobject src, jobject dest) {

    art::mirror::ArtMethod* smeth =

            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

 

    art::mirror::ArtMethod* dmeth =

            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

 

    dmeth->declaring_class_->class_loader_ =

            smeth->declaring_class_->class_loader_; //for plugin classloader

    dmeth->declaring_class_->clinit_thread_id_ =

            smeth->declaring_class_->clinit_thread_id_;

    dmeth->declaring_class_->status_ = (void *)((int)smeth->declaring_class_->status_-1);

    //把一些参数的指针给补丁方法

    smeth->declaring_class_ = dmeth->declaring_class_;

    smeth->access_flags_ = dmeth->access_flags_;

    smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;

    smeth->dex_cache_initialized_static_storage_ =

            dmeth->dex_cache_initialized_static_storage_;

    smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;

    smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;

    smeth->vmap_table_ = dmeth->vmap_table_;

    smeth->core_spill_mask_ = dmeth->core_spill_mask_;

    smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;

    smeth->mapping_table_ = dmeth->mapping_table_;

    smeth->code_item_offset_ = dmeth->code_item_offset_;

    smeth->entry_point_from_compiled_code_ =

            dmeth->entry_point_from_compiled_code_;

 

    smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;

    smeth->native_method_ = dmeth->native_method_;//把补丁方法替换掉

    smeth->method_index_ = dmeth->method_index_;

    smeth->method_dex_index_ = dmeth->method_dex_index_;

 

    LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,

            dmeth->entry_point_from_compiled_code_);

}

添加Patch

mPatchManager.addPatch(path)

源码:

public void addPatch(String path) throws IOException {

        File src = new File(path);

        File dest = new File(mPatchDir, src.getName());

        if (!src.exists()) {

            throw new FileNotFoundException(path);

        }

        if (dest.exists()) {

            Log.d(TAG, "patch [" + path + "] has be loaded.");

            return;

        }

        FileUtil.copyFile(src, dest);// copy to patch's directory

        Patch patch = addPatch(dest);//同loadPatch中的addPatch一样的操作

        if (patch != null) {

            loadPatch(patch);//加载pach,同上loadPatch

        }

    }

移除Patch

mPatchManager.removeAllPatch();

源码:

public void removeAllPatch() {

        cleanPatch();//删除本地缓存的patch文件列表

        SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,

                Context.MODE_PRIVATE);

        sp.edit().clear().commit();//把关于patch的数据进行清空

    }

到此源代码就解析结束。

反编译Patch dex文件代码

patch文件中.dex文件反编译后,看到源码效果如下: 
RTENOTITLE

 

AndFix优缺点和使用心得[编辑]

1、优点:

1)因为是动态的,所以不需要重启应用就可以生效

2)支持ART与Dalvik

3)与multidex方案相比,性能会有所提升(Multi Dex需要修改所有class的class_ispreverified标志位,导致运行时性能有所损失)

4)支持新增加方法

5)支持在新增方法中新增局部变量

6)足够轻量,生成补丁文件简单

7)安全性够高,验证签名

  

2、缺点:

1)因为是动态的,跳过了类的初始化,设置为初始化完毕,所以对于静态方法、静态成员变量、构造方法或者class.forname()的处理可能会有问题

2)不支持新增成员变量和修改成员变量

3)官方apkPatch工具不支持multidex,但是可以通过修改工具来达到支持multidex的目的

4)由于是在native层替换方法,某些缺心眼厂商可能会修改源生关键部分的native层实现,导致可能在某些特定ROM支持不够好

issue(122个,查看了一些issue,记录一些可能存在的问题以及缺陷):

1)兼容性问题

2)部分手机奔溃

3)部分手机ANR

4)不能改变量的值,不过方法的添加修改,删除,都可以

5)需要注意多进程

6)ART下模式无法对同一个方法进行多次更新

 

3、使用心得(总结一下使用AndFix需要注意的地方):

1)合理封装,再包一层

2)PatchManager保持单例

3)初始化PatchManager的Context要用ApplicationContext

4)patch文件后缀为.apatch  

5)patch文件记得用MD5校验

6)一个版本只有patch文件

7)当App升级AndFix会自动删除原有的apath文件,不需要自己动手

8)合理使用try catch来降低AndFix带来的crash概率

9)在使用AndFix的catch块里上报所有的错误,以便观察

10)PatchManager.addPatch(path)要在主线程调用

11)不能对同一个方法修复两次,否则App根本跑不起来

12)aar只有arm,x86的so库,想要兼容更多平台,需要自己再添加相应的so库

13)生成patch的原apk和新apk都只能是realease版本


各大Android热补丁方案分析和比较[编辑]

   最近开源界涌现了很多热补丁项目,但从方案上来说,主要包括 Dexposed 、AndFix 、 ClassLoader (来源是原QZone,现淘宝的工程师陈钟,在15年年初就已经开始实现)三种。前两个都是阿里巴巴内部的不同团队做的(淘宝和支付宝),后者则来自腾讯的QQ空间团队。

开源界往往一个方案会有好几种实现(比如ClassLoader方案已经有不下三种实现了),但这三种方案的原理却徊然不同,那么让我们来看看它们三者的原理和各自的优缺点吧。

Dexposed[编辑]

   基于 Xposed 的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。

Xposed需要Root权限,是因为它要修改其他应用、系统的行为,而对单个应用来说,其实不需要root。 Xposed通过修改Android Dalvik运行时的Zygote进程,并使用Xposed Bridge来hook方法并注入自己的代码,实现非侵入式的runtime修改。比如蜻蜓fm和喜马拉雅做的事情,其实就很适合这种场景,别人反编译市场下载的代码是看不到patch的行为的。 小米 (onVmCreated里面还未小米做了资源的处理)也重用了dexposed,去做了很多自定义主题的功能,还有沉浸式状态栏等。

我们知道,应用启动的时候,都会fork zygote进程,装载class和invoke各种初始化方法,Xposed就是在这个过程中,替换了app_process,hook了各种入口级方法(比如handleBindApplication、ServerThread、ActivityThread、ApplicationPackageManager的getResourcesForApplication等),加载XposedBridge.jar提供动态hook基础(关于Android hook的知识请参考:http://www.ithtw.com/6669.html)。具体到方法,可参见 XposedBridge :

/**
 * Intercept every call to the specified method and call a handler function instead.
 * @param method The method to intercept
 */
private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

其具体native实现则在 Xposed的libxposed_common.cpp 里面有注册,根据系统版本分发到libxposed_dalvik和libxposed_art里面,以dalvik为例大致来说就是记录下原来的方法信息,并把方法指针指向我们的hookedMethodCallback,从而实现拦截的目的。

方法级的替换是指,可以在方法前、方法后插入代码,或者直接替换方法。只能针对java方法做拦截,不支持C的方法。

来说说硬伤吧,不支持art,不支持art,不支持art。重要的事情要说三遍。尽管在6月,项目网站的roadmap就写了7、8月会支持art,但事实是现在还无法解决art的兼容。

另外,如果线上release版本进行了混淆,那写patch也是一件很痛苦的事情,反射+内部类,可能还有包名和内部类的名字冲突,总而言之就是写得很痛苦。

AndFix[编辑]

同样是方法的hook,AndFix不像Dexposed从Method入手,而是以Field为切入点。有关Method、Filed的知识点请参考

http://www.jb51.net/article/77271.htm和 http://blog.csdn.net/abing37/article/details/5250754

先看Java入口, AndFixManager.fix :

/**

 * fix

 *

 * @param file        patch file

 * @param classLoader classloader of class that will be fixed

 * @param classes     classes will be fixed

 */

public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {

    // 省略...判断是否支持,安全检查,读取补丁的dex文件

 

    ClassLoader patchClassLoader = new ClassLoader(classLoader) {

      @Override

      protected Class<?> findClass(String className) throws ClassNotFoundException {

        Class<?> clazz = dexFile.loadClass(className, this);

        if (clazz == null && className.startsWith("com.alipay.euler.andfix")) {

          return Class.forName(className);// annotation’s class not found

        }

        if (clazz == null) {

          throw new ClassNotFoundException(className);

        }

        return clazz;

      }

    };

    Enumeration<String> entrys = dexFile.entries();

    Class<?> clazz = null;

    while (entrys.hasMoreElements()) {

      String entry = entrys.nextElement();

      if (classes != null && !classes.contains(entry)) {

        continue;// skip, not need fix

      }

      // 找到了,加载补丁class

      clazz = dexFile.loadClass(entry, patchClassLoader);

      if (clazz != null) {

        fixClass(clazz, classLoader);

      }

    }

  } catch (IOException e) {

    Log.e(TAG, "pacth", e);

  }

}

看来最终fix是在 fixClass方法 :

private void fixClass(Class<?> clazz, ClassLoader classLoader) {

  Method[] methods = clazz.getDeclaredMethods();

  MethodReplace methodReplace;

  String clz;

  String meth;

  // 遍历补丁class里的方法,进行一一替换,annotation则是补丁包工具自动加上的

  for (Method method : methods) {

    methodReplace = method.getAnnotation(MethodReplace.class);

    if (methodReplace == null)

      continue;

    clz = methodReplace.clazz();

    meth = methodReplace.method();

    if (!isEmpty(clz) && !isEmpty(meth)) {

      replaceMethod(classLoader, clz, meth, 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

      Class<?> clzz = classLoader.loadClass(clz);

      // 这里也很黑科技,通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public,具体可以看Method结构体

      clazz = AndFix.initTargetClass(clzz);

    }

    if (clazz != null) {// initialize class OK

      mFixedClass.put(key, clazz);

      // 需要被替换的函数

      Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());

      // 这里是调用了jni,art和dalvik分别执行不同的替换逻辑,在cpp进行实现

      AndFix.addReplaceMethod(src, method);

    }

  } catch (Exception e) {

    Log.e(TAG, "replaceMethod", e);

  }

}

在dalvik和art上,系统的调用不同,但是原理类似,这里我们尝个鲜,以6.0为例 art_method_replace_6_0 :

// 进行方法的替换

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {

  art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

  art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

 

  dmeth->declaring_class_->class_loader_ =

      smeth->declaring_class_->class_loader_; //for plugin classloader

  dmeth->declaring_class_->clinit_thread_id_ =

      smeth->declaring_class_->clinit_thread_id_;

  dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;

 

  // 把原方法的各种属性都改成补丁方法的

  smeth->declaring_class_ = dmeth->declaring_class_;

  smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;

  smeth->access_flags_ = dmeth->access_flags_;

  smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;

  smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;

  smeth->method_index_ = dmeth->method_index_;

  smeth->dex_method_index_ = dmeth->dex_method_index_;

 

  // 实现的指针也替换为新的

  smeth->ptr_sized_fields_.entry_point_from_interpreter_ =

      dmeth->ptr_sized_fields_.entry_point_from_interpreter_;

  smeth->ptr_sized_fields_.entry_point_from_jni_ =

      dmeth->ptr_sized_fields_.entry_point_from_jni_;

  smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =

      dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

 

  LOGD("replace_6_0: %d , %d",

      smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,

      dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);

}

 

// 这就是上面提到的,把方法都改成public的,所以说了解一下jni还是很有必要的,java世界在c世界是有映射关系的

void setFieldFlag_6_0(JNIEnv* env, jobject field) {

  art::mirror::ArtField* artField =

      (art::mirror::ArtField*) env->FromReflectedField(field);

  artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;

  LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);

}

在dalvik上的实现略有不同,是通过jni bridge来指向补丁的方法。

使用上,直接写一个新的类,会由补丁工具会生成注解,描述其与要打补丁的类和方法的对应关系。

ClassLoader[编辑]

  原腾讯空间Android工程师陈钟发明的热补丁方案。我们知道,multidex方案的实现,其实就是把多个dex放进app的classloader之中,从而使得所有dex的类都能被找到。而实际上findClass的过程中,如果出现了重复的类,参照下面的类加载的实现,是会使用第一个找到的类的。关于ClassLoader基础知识请参考:http://www.trinea.cn/android/java-loader-common-class/ 。

 

public Class findClass(String name, List<Throwable> suppressed) {  

 

    for (Element element : dexElements) {  //每个Element就是一个dex文件

        DexFile dex = element.dexFile;

        if (dex != null) {

            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);

            if (clazz != null) {

              return clazz;

            }

        }

    }

    if (dexElementsSuppressedExceptions != null) {  

        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));

    }

    return null;

}

   该热补丁方案就是从这一点出发,只要把有问题的类修复后,放到一个单独的dex,通过反射插入到dexElements数组的最前面,不就可以让虚拟机加载到打完补丁的class了吗。

说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报preverified错误,原来在 DexPrepare.cpp ,将dex转化成odex的过程中,会在DexVerify.cpp 进行校验,验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。通过在所有类(Application除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex的类的引用,就可以解决这个问题。空间使用了javaassist进行编译时字节码插入。

开源实现有 Nuwa , HotFix , DroidFix 。

比较[编辑]

  Dexposed不支持Art模式(5.0+),且写补丁有点困难,需要反射写混淆后的代码,粒度太细,要替换的方法多的话,工作量会比较大。

  AndFix支持2.3-6.0,但是不清楚是否有一些机型的坑在里面,毕竟jni层不像java曾一样标准,从实现来说,方法类似         Dexposed,都是通过jni来替换方法,但是实现上更简洁直接,应用patch不需要重启。但由于从实现上直接跳过了类初始化,设置为初始化完毕,所以像是静态函数、静态成员、构造函数都会出现问题,复杂点的类Class.forname很可能直接就会挂掉。

  ClassLoader方案支持2.3-6.0,会对启动速度略微有影响,只能在下一次应用启动时生效,在空间中已经有了较长时间的线上应用,如果可以接受在下次启动才应用补丁,是很好的选择。

  总的来说,在兼容性稳定性上, ClassLoader方案很可靠 ,如果需要应用 不重启就能修复 ,而且方法足够简单,可以使用 AndFix ,而 Dexposed由于还不能支持art ,所以只能暂时放弃,希望开发者们可以改进使它能支持art模式,毕竟xposed的种种能力还是很吸引人的(比如hook别人app的方法拿到解密后的数据,嘿嘿),还有比如无痕埋点啊线上追踪问题之类的,随时可以下掉。

 

 

参考:[编辑]


AndFix使用说明  http://www.jianshu.com/p/479b8c7ec3e3

Alibaba-AndFix Bug热修复框架原理及源码解析     http://blog.csdn.net/qxs965266509/article/details/49816007

http://w4lle.github.io/2016/03/03/Android%E7%83%AD%E8%A1%A5%E4%B8%81%E4%B9%8BAndFix%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

http://www.tuicool.com/articles/Fraqeab

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值