欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android热修复之AndFix原理探索(黑科技热修复的Java层实现)

程序员文章站 2024-03-22 08:10:04
...

最近研究了一下阿里的AndFix框架,原理本身不复杂,但是深入探索后发现热修复这块原来有很多底层的知识和黑科技值得挖掘的,故形成本篇blog和大家分享。而AndFix框架本身的使用和集成可直接根据github的README来做,这里就不赘述了。
AndFix项目地址:https://github.com/alibaba/AndFix

先简单说说AndFix的原理,官方的图直接拉过来:
Android热修复之AndFix原理探索(黑科技热修复的Java层实现)

总结起来就一句话:AndFix judges the methods should be replaced by java custom annotation and replaces it by hooking it. AndFix has a native method art_replaceMethod in ART or dalvik_replaceMethod in Dalvik.Andfix框架可以根据安卓的两种不同的虚拟机来实现native层的方法替换,替换的依据是方法上的注解。对于底层不太熟悉或者只做过java的同学,可能不太理解啥叫方法替换,写好的java方法可以被重载被继承还能被替换?所以,作为一个程序员光会某种语言实现某种功能是远远不够的,当你陷入讨论“XX语言是宇宙里最好的语言”的缠斗浪费生命时,不妨多花点时间去学习下计算机和操作系统的原理与思想。这里推荐先去学习两块基础知识:1.C语言指针;2.C语言函数指针(C语言的函数在java里就是方法);3.更有余力的同学还可以去深入学习一下汇编语言的call和ret指令是如何实现类似函数功能的,调用时寄存器的数据是如何压栈保护的。
这里为了方便大家理解,我用一句话来概括AndFix原理的本质:对于计算机来说,一切都只是内存中的数据而已,所以函数也只是内存中的一块数据,方法B替换方法A,其实就是把B中的内容复制到地址A去。
ok,明白了这个我们再去理解代码就轻松多了,先看下方法替换的大致流程:
Android热修复之AndFix原理探索(黑科技热修复的Java层实现)

对应AndFix提供的API我们可以发现,API非常简单,而已这几个流程几乎是一一对应的:

    PatchManager patchManager = new PatchManager(this);
    patchManager.init(appVersion);
    patchManager.loadPatch();
    patchManager.addPatch(path);

首先看构造函数,其实猜也能猜到,一般做API都会用一个外包(观)模式封装一个API invoker,这里的PatchManager显然也是干这个的,核心在AndFixManager里,这里主要做了一些验证工作和文件夹的创建工作,补丁最终存放的地址是context默认的file文件夹下的apatch文件夹中,最后设定mSupport这个值来方便后面的函数确认是否可以实现方法替换:

public PatchManager(Context context) {
        mContext = context;
        mAndFixManager = new AndFixManager(mContext);
        mPatchDir = new File(mContext.getFilesDir(), DIR);
        mPatchs = new ConcurrentSkipListSet<Patch>();
        mLoaders = new ConcurrentHashMap<String, ClassLoader>();
    }
public AndFixManager(Context context) {
        mContext = context;
        mSupport = Compat.isSupport();
        if (mSupport) {
            mSecurityChecker = new SecurityChecker(mContext);
            mOptDir = new File(mContext.getFilesDir(), DIR);
            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;
            }
        }
    }

注意这里有一个很重要的方法Compat的isSupport方法,这里会调用AndFix的setup方法,跟进去是一个native方法,这个方法分别对dalvik虚拟机和art虚拟机的初始化,art虚拟机的初始化很简单,只是记下了apiVersion;而dalvik则相对复杂一些,首先获取dvmDecodeIndirectRef_fnPtr和dvmThreadSelf_fnPtr两个函数,这两个函数可以通过类对象获取ClassObject结构体,然后在native层获取了java层Method方法的getDeclaringClass方法的指针,后面会用到。

public static synchronized boolean isSupport() {
        if (isChecked)
            return isSupport;

        isChecked = true;
        // not support alibaba's YunOs
        if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {
            isSupport = true;
        }

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

        return isSupport;
    }
static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
      jint apilevel) {
   isArt = isart;
   LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
         (int )apilevel);
   if (isArt) {
      return art_setup(env, (int) apilevel);
   } else {
      return dalvik_setup(env, (int) apilevel);
   }
}

extern jboolean __attribute__ ((visibility ("hidden"))) art_setup(JNIEnv* env,
      int level) {
   apilevel = level;
   return JNI_TRUE;
}

extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
      JNIEnv* env, int apilevel) {
   //打开系统的"libdvm.so"文件
   void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
   if (dvm_hand) {

      dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
            apilevel > 10 ?
                  "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
                  "dvmDecodeIndirectRef");
      if (!dvmDecodeIndirectRef_fnPtr) {
         return JNI_FALSE;
      }
      dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
            apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
      if (!dvmThreadSelf_fnPtr) {
         return JNI_FALSE;
      }
      //通过Java层Method对象的getDeclaringClass方法
      //后续会调用该方法获取某个方法所属的类对象
      //因为Java层只传递了Method对象到native层
      jclass clazz = env->FindClass("java/lang/reflect/Method");
      jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
                  "()Ljava/lang/Class;");

      return JNI_TRUE;
   } else {
      return JNI_FALSE;
   }
}

然后看init方法,还是先创建补丁存放的文件夹,然后从SP中取出当前app的version,如果version发生了变更或者第一次的时候没有这个值,则清楚当前补丁目录下的所有文件;如果version存在且和当前传入的version值匹配,则初始化所有的补丁。可以看到initPatchs方法最终会调用到addPatch,注意这里的addPatch是一个private方法,而不是给我们外面调用的API,这个方法就是遍历补丁文件夹,然后把路径下以.apatch为后缀的补丁文件进行解析,解析后的数据全部封装到Patch这个类中。

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();
        }
    }
private void initPatchs() {
        File[] files = mPatchDir.listFiles();
        for (File file : files) {
            addPatch(file);
        }
    }
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;
    }
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);
            JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
            inputStream = jarFile.getInputStream(entry);
            Manifest manifest = new Manifest(inputStream);
            Attributes main = manifest.getMainAttributes();
            mName = main.getValue(PATCH_NAME);
            mTime = new Date(main.getValue(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();
                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();
            }
        }

    }

所有patch文件解析完成后,下一步就是调用loadPatch方法,其本质就是把已经拷贝到PatchDir下的补丁进行调用。可以看到这里最终会调用mAndFixManager的fix方法,fix方法我们后面再看,先考虑一下我们PatchDir下的文件是从哪里来的呢?没错,答案就在最后一个API里。

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);
                mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                        classes);
            }
        }
    }

接下来再看一下addPatch的API,这个方法把我们下载到sd卡下的补丁文件复制到patchDir下面,然后就走了和loadPatch一样的流程,最终都会走到mAndFixManager的fix方法中。

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

而AndFix的fix方法最终会调用到native层的replaceMethod方法实现方法替换。

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


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

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是通过注解来获取要被替换的方法,这个注解是从哪里来的呢?大家看AndFix的集成文档可以看到这段:How to use?Prepare two android packages, one is the online package, the other one is the package after you fix bugs by coding.Generate .apatch file by providing the two package。没错,就是在生成补丁文件的时候把发生改变的类增加了一个CF后缀,然后把对应的方法动态加上了注解,最后丢到了补丁中的dex文件中,我们反编译一下这个dex文件,看看AndFix帮我们生成的类:
Android热修复之AndFix原理探索(黑科技热修复的Java层实现)

可以看到,AndFix自动帮我们加上了一个methodReplace的注解,注解里的内容就是要被替换的原类中的类名和方法。最后我们看一下native层真正做的事情,Android的java运行环境,在4.4以下用的是dalvik虚拟机,而在4.4以上用的是art虚拟机,所以native层针对两种不同的虚拟机做了区分:

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

先看dalvik,主要就是通过前文中获取到的两个方法拿到了dest的ClassObject结构体对象,然后更改为类初始化完成的状态,随后进行了Method结构体的指针替换,把原来apk中的方法替换成补丁中的,完成了热修复。

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的,不同的art系统版本不同处理也不同,这里以5.0为例,其实不同版本的实现原理是一样的,使用FromReflectedMethod方法拿到Java层Method对应native层的ArtMethod指针,然后执行替换的(前面的dalvik虚拟机也是用的这个方法拿到的JNI层的Method指针)。


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

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

}

好,知道了AndFix的原理,我们该上点黑科技了,从上面的代码我们也可以发现,这样的底层替换会带来一个致命的问题就是兼容性的问题。虽然Andfix是把底层结构强转为了art::mirror::ArtMethod,但这里的art::mirror::ArtMethod并非等同于app运行时所在设备虚拟机底层的art::mirror::ArtMethod,而是Andfix自己构造的art::mirror::ArtMethod。看一下AndFix中的ArtMethod源码我们可以发现,是和Android源码中的ArtMethod一模一样的,所以如果某个厂商修改了这个源码,那么在替换的时候就会出问题,怎么办,难道每个rom再做一次适配吗?如果人家不开源怎么办?所以,阿里的大神们更换了思维模式,既然目标是把原来的ArtMethod的内容全部替换新的ArtMethod中的,那就直接换ArtMethod指针好了,上图,原来是:
Android热修复之AndFix原理探索(黑科技热修复的Java层实现)

换成:
Android热修复之AndFix原理探索(黑科技热修复的Java层实现)

用C语言代码来描述就是memcpy(smeth, dmeth, sizeof(ArtMethod));
就是这样,一句话就能取代上面一堆代码。刚才提到过,不同的手机厂商都可以对底层的ArtMethod进行任意修改,但即使他们把ArtMethod改得六亲不认,只要我像这样把整个ArtMethod结构体完整替换了,就能够把所有旧方法成员自动对应地换成新方法的成员。听上去是不是很美。别着急往下看。
但这其中最关键的地方,在于sizeof(ArtMethod)。如果size计算有偏差,导致部分成员没有被替换,或者替换区域超出了边界,都会导致严重的问题。对于ROM开发者而言,是在art源代码里面,所以一个简单的sizeof(ArtMethod)就行了,因为这是在编译期就可以决定的。但我们是上层开发者,app会被下发给各式各样的Android设备,所以我们是需要在运行时动态地得到app所运行设备上面的底层ArtMethod大小的,这就没那么简单了。想要忽略ArtMethod的具体结构成员直接取得其size的精确值,我们还是需要从虚拟机的源码入手,从底层的数据结构及排列特点探寻答案。在art里面,初始化一个类的时候会给这个类的所有方法分配空间,我们可以看到这个分配空间的地方:

void ClassLinker::LoadClassMembers(Thread* self, const DexFile& dex_file,
                                   const uint8_t* class_data,
                                   Handle<mirror::Class> klass,
                                   const OatFile::OatClass* oat_class) {
    ... ...

    ArtMethod* const direct_methods = (it.NumDirectMethods() != 0)
        ? AllocArtMethodArray(self, it.NumDirectMethods())
        : nullptr;
    ArtMethod* const virtual_methods = (it.NumVirtualMethods() != 0)
        ? AllocArtMethodArray(self, it.NumVirtualMethods())
        : nullptr;                                   

    ... ...                                

类的方法有direct方法和virtual方法。direct方法包含static方法和所有不可继承的对象方法。而virtual方法就是所有可以继承的对象方法了。AllocArtMethodArray函数分配了他们所需的内存空间。

ArtMethod* ClassLinker::AllocArtMethodArray(Thread* self, size_t length) {
  const size_t method_size = ArtMethod::ObjectSize(image_pointer_size_);
  uintptr_t ptr = reinterpret_cast<uintptr_t>(
      Runtime::Current()->GetLinearAlloc()->Alloc(self, method_size * length));
  CHECK_NE(ptr, 0u);
  for (size_t i = 0; i < length; ++i) {
    new(reinterpret_cast<void*>(ptr + i * method_size)) ArtMethod;
  }
  return reinterpret_cast<ArtMethod*>(ptr);
}

其实不用看代码就能推断出,既然是数组,那一定是一片连续的内存,所以ArtMethod在内存中一定是连续排列的。正是这里给了我们启示,ArtMethod们是连续排列的,所以一个ArtMethod的大小,不就是相邻两个方法所对应的ArtMethod的起始地址的差值吗?
好了,万事具备,其实native层的ArtMethod结构体直接替换阿里已经在AndFix的升级框架HotFix中完成了,这里就不再赘述,有兴趣的同学可以去找相关的blog来看,剩下的就是如本文标题所述,我们来一点黑科技,在java层实现native层才能实现的方法替换功能。并且自己做一个简单的热修复框架。
要在java层做这件事,我们先看一下native层是怎么拿到ArtMethod的指针的,前面的代码里我们知道,是通过一个FromReflectedMethod方法,我们看一下源码:

ArtMethod* ArtMethod::FromReflectedMethod(const ScopedObjectAccessAlreadyRunnable& soa,
                                          jobject jlr_method) {
  mirror::ArtField* f =
      soa.DecodeField(WellKnownClasses::java_lang_reflect_AbstractMethod_artMethod);
  mirror::ArtMethod* method = f->GetObject(soa.Decode<mirror::Object*>(jlr_method))->AsArtMethod();
  DCHECK(method != nullptr);
  return method;
}

从源码中我们可以看到,我们在native层替换的那个 ArtMethod 不是在 Java 层也有对应的东西么?我们直接替换掉 Java 层的这个artMethod 字段不就OK了?但是java有memcpy吗,java为了面向对象,不是把所有的内存的操作都封装起来了吗,难道有后门?哈,还真的有,JDK给我们留了一个后门:sun.misc.Unsafe 类;在OpenJDK里面这个类灰常强大,从内存操作到CAS到锁机制,无所不能。但是在Android 平台还有一点点不一样,在 Android N之前,Android的JDK实现是 Apache Harmony,这个实现里面的Unsafe就有点鸡肋了,没法写内存,好在Android 又开了一个后门:Memory类。但是这个类是隐藏的,所以我们要用的话,只能通过反射。这里我们做一个封装。方法具体的作用请参考注释。

private static class Memory
{

    //从address这个地址中取出byte数据
    static byte peekByte(long address)
    {
        return (Byte)ReflectUtils.invokeStaticMethod(
                "libcore.io.Memory", "peekByte", new Class[]{long.class}, new Object[]{address}
            );

    }

    //把一个byte数据写到address地址中
    static void pokeByte(long address, byte value)
    {
        ReflectUtils.invokeStaticMethod(
                    "libcore.io.Memory", "pokeByte",
                    new Class[]{long.class, byte.class}, new Object[]{address, value}
        );

    }

    //从src地址中取出byte放入到dst地址中,共length个字节
    static void memcpy(long dst, long src, long length)
    {
        for (long i = 0; i < length; i++)
        {
            pokeByte(dst, peekByte(src));
            dst++;
            src++;
        }
    }
}

    private static class Unsafe
    {
        static final String UNSAFE_CLASS_NAME = "sun.misc.Unsafe";
        static Object UNSAFE_CLASS_INSTANCE =
                ReflectUtils.getStaticFieldObj(UNSAFE_CLASS_NAME, "THE_ONE");

        //获取类的内存地址
        static long getObjectAddress(Object obj)
        {
            Object[] args = {obj};

            Integer baseOffset = (Integer) ReflectUtils.invokeMethod(
                    UNSAFE_CLASS_NAME, UNSAFE_CLASS_INSTANCE, "arrayBaseOffset",
                    new Class[]{Class.class}, new Object[]{Object[].class}
            );

            long result = ((Number)ReflectUtils.invokeMethod(
                    UNSAFE_CLASS_NAME, UNSAFE_CLASS_INSTANCE, "getInt",
                    new Class[]{Object.class, long.class}, new Object[]{args, baseOffset.longValue()}
            )).longValue();

            return result;
        }

    }

OK,memcpy有了,下面就剩一个length了,两个ArtMethod之间相距有多少字节呢,既然java没有sizeof,那我们可以从前面推倒的原理入手,只需要创建一个数组,丢两个ArtMethod,把两个数组元素的起始地址相减不就得到一个artMethod的大小了吗?请看源码,这里我们还封装了一个获取Method中artMethod地址的方法。

    private static class MethodDecoder
    {
        static long sMethodSize = -1;

        public static void ruler1()
        {
        }

        public static void ruler2()
        {
        }

        static long getMethodAddress(Method method)
        {

            Object mirrorMethod =
                    ReflectUtils.getFieldObj(
                            Method.class.getSuperclass().getName(), method, "artMethod"
                    );
            if (mirrorMethod.getClass().equals(Long.class))
            {
                return (Long) mirrorMethod;
            }

            return Unsafe.getObjectAddress(mirrorMethod);
        }

        static long getArtMethodSize()
        {
            if (sMethodSize > 0)
            {
                return sMethodSize;
            }

            try
            {
                Method m1 = MethodDecoder.class.getDeclaredMethod("ruler1");
                Method m2 = MethodDecoder.class.getDeclaredMethod("ruler2");

                sMethodSize = getMethodAddress(m2) - getMethodAddress(m1);
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }

            return sMethodSize;
        }
    }

好了,万事俱备,现在我们只要提供一个replaceMethod方法作为API给外部就OK了:

    public static void replaceMethod(Method origin, Method replace)
    {
        long addressOrigin = MethodDecoder.getMethodAddress(origin);
        long addressReplace = MethodDecoder.getMethodAddress(replace);
        Memory.memcpy(
                addressOrigin,
                addressReplace,
                MethodDecoder.getArtMethodSize());
    }

外层调用的时候只要直接使用这个方法,传入两个方法的Method对象,就能实现method的动态替换,是不是超级神奇?
这篇先到这,已经很长了就不往下拓展了,下一篇我们会根据以上的知识和AndFix补丁的原理,实现一个简单的java层热修复框架。谢谢观赏。

相关标签: 热修复 Android

上一篇: 三级联动案列

下一篇: