码迷,mamicode.com
首页 > 其他好文 > 详细

Andfix源码分析

时间:2018-02-22 23:48:29      阅读:194      评论:0      收藏:0      [点我收藏+]

标签:源码   erp   inter   外部存储   原理   otn   names   遍历   循环调用   

Andfix源码分析

 

缩写:

ART

 

 

Andfix是阿里巴巴推出的一款基于Method Hook的热修复技术,目前Github点赞数5.7K,是一款安全性高,较为稳定,性能比较优异的方法级替换的热修复技术。代码实现上条理清晰,架构设计合理,可读性强,是一个实现上非常优雅的开源框架。下面我们重点介绍下Andfix的源码及其设计。

 

 

一个经典的开源框架首先要友好的对外暴露接口,这样才能更便于接入,实现快速启动。所以,在介绍核心源码之前,我们首先关注下Andfix的外部接口部分。

MainApplication

 

为了尽可能的覆盖BUG修复的范围,和其他的热修复技术一样,Andfix选择在APP启动的时候对热补丁进行加载,也即Application的OnCreate过程。整体的外部接口调用如下所示:

 

@Override

    public void onCreate() {

        super.onCreate();

        // patch的初始化

        mPatchManager = new PatchManager(this);

        mPatchManager.init("1.0");

        Log.d(TAG, "inited.");

 

        // 加载缓存中的patch

        mPatchManager.loadPatch();

        Log.d(TAG, "apatch loaded.");

 

        // 将外部存储中的patch加载到当前运行的ART中

        try {

            // .apatch file path

            String patchFileString = Environment.getExternalStorageDirectory()

                    .getAbsolutePath() + APATCH_PATH;

            mPatchManager.addPatch(patchFileString);

            Log.d(TAG, "apatch:" + patchFileString + " added.");

        } catch (IOException e) {

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

        }

    }

 

这部分接口非常简洁,大概分为三步:patch的初始化,patch的缓存加载,patch的外部存储加载。

缓存加载是为了加载之前已经从外部存储载入到缓存(data目录下)中的patch,外部存储加载是为了从外部存储中加载patch到缓存。Andfix的整体外部调用就是上面的几步,下面我们来看下Andfix的具体实现部分。

 

 

 

Andfix的具体实现上主要分为三部分:Patch管理部分,Fix管理部分,Native Hook部分,其整体的UML架构图如下所示:

 

 

 

 

 

PatchManager

 

整体的初始化函数的源码如下:

/**

     * Patch的初始化工作

     * @param appVersion App的版本号

     */

    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);

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

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

            cleanPatch();

            sp.edit().putString(SP_VERSION, appVersion).commit();

        } else {

            initPatchs();

        }

    }

 

其中mPatchDir表示data私有目录下存放Patch文件的文件夹。首先是关于mPatchDir的简单文件夹操作,在mPatchDir文件夹初始化完成之后,紧接着比较当前的APP版本和SharedPreferences中保存的Patch对应的APP版本,两者如果不相等的话,会直接清除掉本地缓存的Patch文件和对应Patch相关的数据。这是因为热补丁是跟APP强相关的,Patch只能精确的修复对应版本的Bug。清除的源码如下所示:

 

private void cleanPatch() {

        File[] files = mPatchDir.listFiles();

        for (File file : files) {

            mAndFixManager.removeOptFile(file);

            if (!FileUtil.deleteFile(file)) {

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

            }

        }

    }

 

 

在版本号匹配之后,紧接着是Patch文件的初始化部分(initPatchs()),其源码如下:

 

 

    private void initPatchs() {

        File[] files = mPatchDir.listFiles();

        for (File file : files) {

            addPatch(file);

        }

    }

 

在上述函数中,ART会遍历Patch文件,并将Patch文件通过addPatch方法添加到内存中。

addPatch方法有两种多态实现,分别如下:

  • private Patch addPatch(File file)
  • public void addPatch(String path) throws IOException

其中第一个方法是从Patch文件中获取Patch对象,源码如下:

 

具体的源码如下:

 

    /**

     * add patch file

     *

     * @param file

     * @return patch

     */

    private Patch addPatch(File file) {

        Patch patch = null;

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

            try {

                patch = new Patch(file);

                mPatchs.add(patch);

            } catch (IOException e) {

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

            }

        }

        return patch;

    }

 

此方法中把Patch文件夹映射为Patch对象,然后将Patch对象统一存放在mPatchs数据集里面。

第二个方法是从本地路径中获取Patch文件,然后从Patch文件中解析出Patch对象,之后触发Patch的加载过程,具体源码如下:

    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);

        if (patch != null) {

            loadPatch(patch);

        }

    }

 

 

 

获取完Patch的对象列表之后,接下来的内容就是加载Patch中的内容,并根据Patch中的内容进行Hotfix。此过程是通过Patchmanager类中的loadPatch方法实现的。loadPatch方法一共有三个多态,分别如下:

  • public void loadPatch(String patchName, ClassLoader classLoader)
  • public void loadPatch()
  • private void loadPatch(Patch patch)

 

三个方法入参不同,会通过不同的ClassLoader加载不同的Patch文件,已第三个方法为例,该函数中对数据进行封装之后,最终会循环调用AndfixManager中的fix方法,具体的源码如下:

private void loadPatch(Patch patch) {

        Set<String> patchNames = patch.getPatchNames();

        ClassLoader cl;

        List<String> classes;

        for (String patchName : patchNames) {

            if (mLoaders.containsKey("*")) {

                cl = mContext.getClassLoader();

            } else {

                cl = mLoaders.get(patchName);

            }

            if (cl != null) {

                classes = patch.getClasses(patchName);

                mAndFixManager.fix(patch.getFile(), cl, classes);

            }

        }

    }

 

PatchManager的源码基本就如上所述,主要是对Patch的管理与加载过程,代码简洁易懂,可读性强。

 

接下来,我们重点分析下AndfixManager类,该类中主要介绍Andfix的BugFix的核心流程。通过之前的PatchManager类的源码分析可知,AndfixManager的关键入口函数为fix方法。其源码如下所示:

 

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

        if (!mSupport) {

            return;

        }

 

        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;

                }

            }

 

            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 {

                    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);

                if (clazz != null) {

                    fixClass(clazz, classLoader);

                }

            }

        } catch (IOException e) {

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

        }

    }

在此方法中,主要包括安全校验,bugFix两部分,具体如下;

 

  1. 安全校验

Andfix会对传进来的Patch文件进行安全校验,包括准确性校验和完整性校验。

 

安全校验的具体实现在SecurityChecker类中,其中准确性校验(签名校验)的具体实现如下:

 

/**

     * @param path

     * Apk file

     * @return true if verify apk success

     */

    public boolean verifyApk(File path) {

        if (mDebuggable) {

            Log.d(TAG, "mDebuggable = true");

            return true;

        }

 

        JarFile jarFile = null;

        try {

            jarFile = new JarFile(path);

 

            JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);

            if (null == jarEntry) {// no code

                return false;

            }

            loadDigestes(jarFile, jarEntry);

            Certificate[] certs = jarEntry.getCertificates();

            if (certs == null) {

                return false;

            }

            return check(path, certs);

        } catch (IOException e) {

            Log.e(TAG, path.getAbsolutePath(), e);

            return false;

        } finally {

            try {

                if (jarFile != null) {

                    jarFile.close();

                }

            } catch (IOException e) {

                Log.e(TAG, path.getAbsolutePath(), e);

            }

        }

    }

 

 

 

    // verify the signature of the Apk

    private boolean check(File path, Certificate[] certs) {

        if (certs.length > 0) {

            for (int i = certs.length - 1; i >= 0; i--) {

                try {

                    certs[i].verify(mPublicKey);

                    return true;

                } catch (Exception e) {

                    Log.e(TAG, path.getAbsolutePath(), e);

                }

            }

        }

        return false;

    }

 

上述过程对APK进行证书签名校验,符合签名的APK为合法的APK,否则为非法的APK,中断热修复过程。

 

Andfix的过程不仅进行签名校验,还进行完整性校验。完整性校验是为了防止出现在进行patch下载的过程中下载不完整,导致修复出现异常的情况。完整性校验是通过校验MD4来实现的,具体如下;

    /**

     * @param path

     * Dex file

     * @return true if verify fingerprint success

     */

    public boolean verifyOpt(File file) {

        String fingerprint = getFileMD5(file);

        String saved = getFingerprint(file.getName());

        if (fingerprint != null && TextUtils.equals(fingerprint, saved)) {

            return true;

        }

        return false;

    }

 

 

  1. BugFix

 

 

 

Andfix热修复的核心实现中,分为两个步骤:

  1. 找到需要修复的Class;
  2. 替换需要进行修复的Method。

 

第一步的具体实现如下:

/**

     * fix class

     *

     * @param clazz

     * class

     */

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

        Method[] methods = clazz.getDeclaredMethods();

        MethodReplace methodReplace;

        String clz;

        String meth;

        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);

            }

        }

    }

 

 

第二部的具体实现如下:

    /**

     * 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());

                AndFix.addReplaceMethod(src, method);

            }

        } catch (Exception e) {

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

        }

    }

 

其中核心函数AndFix.addReplaceMethod(src, method)的具体实现如下:

/**

     * replace method‘s body

     *

     * @param src

     * source method

     * @param dest

     * target method

     *

     */

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

        try {

            replaceMethod(src, dest);

            initFields(dest.getDeclaringClass());

        } catch (Throwable e) {

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

        }

    }

 

可以观察到,Andfix中函数的替换是通过Native方法replaceMethod(Method dest, Method src)实现的。从JNI中找到这部分的源码如下:

static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,

        jobject dest) {

    if (isArt) {

        art_replaceMethod(env, src, dest);

    } else {

        dalvik_replaceMethod(env, src, dest);

    }

}

 

发现对于不同的虚拟机,调用的Native方法是不一样的,ART(Android Running Time)虚拟机调用的是art_replaceMethod(env, src, dest)方法;Dalvik虚拟机调用的是dalvik_replaceMethod(env, src, dest)方法。

 

对于ART虚拟机,不同Android API的系统,会适配不同的实现,其代码如下:

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

        JNIEnv* env, jobject src, jobject dest) {

if (apilevel > 23) {

replace_7_0(env, src, dest);

} else if (apilevel > 22) {

        replace_6_0(env, src, dest);

    } else if (apilevel > 21) {

        replace_5_1(env, src, dest);

    } else if (apilevel > 19) {

        replace_5_0(env, src, dest);

}else{

replace_4_4(env, src, dest);

}

}

 

不同API的实现类如下:

技术分享图片

 

所以说Andfix可以兼容到Android 7.0,对于超过Android7.0的版本,目前使用的替换方法和7.0保持一致。

 

具体的替换实现如下所示,以7.0版本为例:

void replace_7_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);

 

//    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ =

//            reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader

    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =

            reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;

    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =

            reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1;

    //for reflection invoke

    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;

 

    smeth->declaring_class_ = dmeth->declaring_class_;

    smeth->access_flags_ = dmeth->access_flags_ | 0x0001;

    smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;

    smeth->dex_method_index_ = dmeth->dex_method_index_;

    smeth->method_index_ = dmeth->method_index_;

    smeth->hotness_count_ = dmeth->hotness_count_;

 

    smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =

            dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;

    smeth->ptr_sized_fields_.dex_cache_resolved_types_ =

            dmeth->ptr_sized_fields_.dex_cache_resolved_types_;

 

    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_7_0: %d , %d",

            smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,

            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);

 

}

 

 

 

 

 

 

评价

 

 

 

 

总结

 

本文对Andfix的原理进行了分析介绍,并对Andfix客户端的源码实现进行了简要分析,其中重点介绍了客户端在获取Patch后进行Class匹配与Method替换的过程。

初次此外,在开发过程中,有几个技术细节也有较大的可挖掘性,具体如下:

  1. 热修复Patch的生成过程;
  2. Patch的下载流程,更新,版本管理;
  3. MultiDex下的Andfix;
  4. ClassLoader的内核原理;
  5. Android Running Time与Dalvik。

Andfix源码分析

标签:源码   erp   inter   外部存储   原理   otn   names   遍历   循环调用   

原文地址:https://www.cnblogs.com/lzhen/p/8460408.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!