这是一个native方法, 它的参数是在 Java层通过反射机制得到的 Method
, src对应被替换的原有方法,而dest对应的就是新方法,新方法存在于补丁包的新类中,也就是补丁方法。
// AndFix/jni/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);
}
}
这个方法对当前Android版本做了判断,4.4以下版本用的是Dalvik虚拟机,而在 4.4以上则用的是ART虚拟机。
在Art上调用的方法如下:
// AndFix/jni/art/art_method_replace.cpp
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);
}
}
我们以 Art虚拟机为例,对于不同Android版本的Art虚拟机,底层Java对象的数据结构是不同的,因而会进一步区分出不同的替换函数,这里我们以Android 6.0版本为例子,对应的就是 replace_6_0()
// AndFix/jni/art/art_method_replace_6_0.cpp
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src); // 1
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest); // 2
…
// 3
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
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->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_);
}
注释1、2:通过Method对象得到底层Java函数对应ArtMethod的真实地址。
注释3:把就函数的所有成员变量都替换为新的。(通过ArtMehtod结构体)(注意注意!这里相当于是替换了原结构体中的字段信息)
每一个Java方法在Art虚拟机中都对应着一个 ArtMethod
,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等。
通过 env->FromReflectedMethod
,可以由 Method对象得到这个方法所对应的 ArtMethod的真正其实地址,然后就可以把它强制转化为ArtMethod指针,从而对其包含的所有成员进行修改。
这样全部替换完之后就完成了热修复功能。以后调用方法时就会直接运行到新方法中实现。
为什么这样替换完就可以实现热修复呢?这需要从虚拟机调用方法的原理说起。
在Android6.0版本中,Art虚拟机中的 Art虚拟机中ArtMethod的结构如下:
// art/runtime/art_method.h
class ArtMethod FINAL {
…
protected:
GcRootmirror::Class declaring_class_;
GcRootmirror::PointerArray dex_cache_resolved_methods_;
GcRoot<mirror::ObjectArraymirror::Class> dex_cache_resolved_types_;
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PACKED(4) PtrSizedFields {
void* entry_point_from_interpreter_; // 1
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_; //2
} ptr_sized_fields_;
…
}
在 ArtMethod结构体中,最重要的就是 注释1和注释2标注的内容,从名字可以看出来,他们就是方法的执行入口。
我们知道,Java代码在Android中会被编译为 Dex Code。
Art虚拟机中可以采用解释模式或者 AOT机器码模式执行 Dex Code
- 解释模式
就是去除Dex Code,逐条解释执行。
如果方法的调用者是以解释模式运行的,在调用这个方法时,就会获取这个方法的 entry_point_from_interpreter_
,然后跳转执行。
- AOT模式
就会预先编译好 Dex Code对应的机器码,然后在运行期直接执行机器码,不需要逐条解释执行Dex Code。如果方法的调用者是以AOT机器码方式执行的,在调用这个方法时,就是跳转到 entry_point_from_quick_compiled_code_
中执行。
那是不是只需要替换这个几个 entry_point_* 入口地址就能够实现方法替换了呢?
并没有那么简单,因为不论是解释模式还是AOT模式,在运行期间还会需要调用ArtMethod中的其他成员字段。
就以AOT模式为例,虽然Dex Code已经被编译成了机器码。但是机器码并非可以脱离虚拟机而单独运行,以下面这段简单的代码为例。
public class MainActivity extends Activity {
protected void onCreate(Bundle saveInstanceState) {
super.onCreate(saveInstanceState);
}
}
编译为 AOT机器码后,是这样的:
7:void com.rikkatheworld.demo.MainActivity.onCreate(android.os.Bundle) (dex_method_idx)
DEX CODE:
0x0000: 6f20 4600 1000 | invoke-super {v0, v1}, void anroid.App.Activity.onCreate(android.os.Bundle)
0x0003: 0e00 | return-void
CODE: (code_offset=0x006fdbac size_offset=0x006fdba8 size=96)
… …
0x006fdbe0: f94003e0 ldr x0, [sp]
;x0 = MainActivity.onCreate 对应的 ArtMethod指针
0x006fdbe4: b9400400 ldr w0, [x0, #4]
;w0 = [x0 + 4] = dex_cache_resolved_methods_ 字段
0x006fdbe8: f9412000 ldr x0, [w0, #576]
;x0 = [x0 + 576]; dex_cache_resolved_methods_ 数组的第72(=576/8)个元素,即对应 Activity.onCreate的 ArtMethod指针
0x006fdbec: f940181e ldr lr, [x0, #48]
;lr = [x0 + 48] = Activity.onCreate 的ArtMethod成员的执行入口点
;即 entry_point_from_quick_compiled_code_
0x006fdbf0: d63f03c0 blr lr
;调用 Activity.onCreate
这里去掉了一些校验之类的无关代码,可以看到,在调用一个方法时,获取了 ArtMethod中的 dex_cache_resolved_methods
文件,这是一个存放 ArtMethod* 的指针数组,通过它就可以访问到这个 Method所在Dex中所有的 Method所对应的 ArtMethod*
Activity.onCreate() 的方法索引是70,由于是64位系统,因此每个指针的大小为8kb,又由于ArtMethod*元素时从这个数组的第0×2 个位置开始存放的,因此偏移量为 (70 + 2) * 8 = 576的位置正是 Activity.onCreate() 方法的 ArtMethod指针。
这只是一个比较简单的例子,而在实际代码中,有许多更为复杂的调用情况,很多情况下还需要调用 dex_code_item_offset_
字段。由此可以看出,AOT机器码的执行过程,还是会有对虚拟机以及ArtMethod成员字段的依赖。
因此,当把一个旧方法的所有成员字段都换为新方法的成员字段后,执行时所有的数据就可以保持和新方法的数据一致。这样在所有执行到旧方法的地方,会获取新方法的执行入口、所属类型、方法索引号以及所属dex信息,然后像调用旧方法一样去执行新方法的逻辑。
然而,目前市场上几乎所有的native替换方案,比如 Andfix和其他安全界的Hook方案,ArtMethod结构体的结构都是固定的,这回带来巨大的兼容性问题。
从刚才的分析可以看到,虽然Andfix是把底层结构强转为 art::mirror::ArtMethod,但这里的 art::mirror::ArtMethod
并非等同于App运行时所在设备虚拟机底层的 art::mirror::ArtMethod
,而是 Andfix自己构造的 art::mirror::ArtMethod
。
由于 Android的源码是开源的,所以各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码中的结构编写的。如果某个厂商对这个 ArtMethod结构体进行了更改,就和原有的开源代码里的结构不一致,那么在这个修改过ArtMethod结构体的设备上,替换机制就会出现问题。
举个例子,在Andfix替换 declaring_class_
的地方:
//这里是设备本身的ArtMethod和AndFix的ArtMethod一样的情况下
smeth->declaring_class_ = demth->declaring_class;
由于 declaring_class_
是 Andfix里 ArtMethod的第一个成员,因此它和以下这行代码等价:
(uint32_t) (smth + 0) = (uint32_t) (dmeth + 0)
如果某个手机厂商在 ArtMethod结构体的 declaring_class_
前面添加了一个字段 additional_
,那么 additional_
就成为了 ArtMethod的第一个成员,所以 smeth + 0 这个位置在这台设备上实际就变成了 additional_
,所以这行代码的真正含义就变成了:
//这里是设备本身的ArtMethod和AndFix的ArtMethod不一样的情况下
smeth->additional_ = dmeth->additional_;
这样就和原有的替换逻辑不一致了。
这也正是Andfix不支持所有机型的原因,很大的可能,是因为这些手机机型修改了底层的虚拟机结构
============================================================================
知道了 native替换方式兼容性问题的原因,我们是否有办法寻求一种新的方式,不依赖ROM底层方法结构的实现而达到替换效果呢?
我们发现,这样的native层面替换思路,其实就是替换 ArtMethod的所有成员,那么,并不需要构造出ArtMethod具体的各个成员字段,只要把 ArtMethod作为整体进行替换,这样不就可以了吗?
因此 Andfix这一系列烦琐的替换:
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
…
可以缩写成:
memcpy(smeth, dmeth, sizeof(ArtMethod));
这正是 Sophix为了解决Andfix的问题,而研发出的新的方案。
刚才提到过,不同的手机厂商都可以对 ArtMethod
进行替换,只要像这样吧 ArtMethod
整个结构体完整替换,就能够把所有旧方法成员自动对应的换成新方法的成员。
但这其中最关键的地方在于,sizeof(ArtMethod)
的计算结果,如果计算结果有偏差,导致部分成员没有被替换,或者替换区域超出了边界,都会导致严重的问题。
对于ROM开发者而言,是在 Art源代码中开发,所以一个简单的 sizeof(ArtMethod)就行了,因为这是在编译器就可以决定的。
但对于上层开发者,App会被下发给各式各样的Android设备,所以需要在运行时动态地获取App运行设备中的底层 ArtMethod大小的,就没有那么简单了。
想要忽略ArtMethod具体结构成员直接获取其size的精确值,还是需要从虚拟机的源码入手,从底层的数据结构以及排列特点探寻答案。
在 Art中,初始化一个类的时候会给这个类的所有方法分配内存空间,我们可以看到这个分配内存空间的地方(Android 8.0代码):
// art/runtime/class_linker.cc
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handlemirror::Class klass) {
{
…
klass->SetMethodsPtr(
AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),
it.NumDirectMethods(),
it.NumVirtualMethods());
…
}
类的方法中有 direct方法和 virtual方法。
direct方法包含static方法和所有不可继承的对象方法。而 virtual方法包含了所有可以继承的对象方法。
AllocArtMethodArray()
函数用来分配他们的方法所在区域,第三个参数传的是所有的direct方法和virtual的总数:
// class-linker.cc
LengthPrefixedArray* ClassLinker::AllocArtMethodArray(Thread* self,
LinearAlloc* allocator,
size_t length) {
if (length == 0) {
return nullptr;
}
const size_t method_alignment = ArtMethod::Alignment(image_pointer_size_);
const size_t method_size = ArtMethod::Size(image_pointer_size_);
const size_t storage_size =
LengthPrefixedArray::ComputeSize(length, method_size, method_alignment);
void* array_storage = allocator->Alloc(self, storage_size);
auto* ret = new (array_storage) LengthPrefixedArray(length);
CHECK(ret != nullptr);
// 1
for (size_t i = 0; i < length; ++i) {
new(reinterpret_cast<void*>(&ret->At(i, method_size, method_alignment))) ArtMethod;
}
return ret;
}
注释1中,建立所有方法总数和的一个循环。每次循环使用 ret->(i, method_size, method_alignment)
创建出一个ArtMethod。
而这个方法的作用就是分配空间,因为i是连续的,所以分配出来的空间也是连续的。
这是只是分配出内存空间,还没有对ArtMethod的各个成员赋值,不过这并不影响观察ArtMethod的空间结构,Artmethod空间结构如下图所示:
这里给了我们启示,ArtMethod是紧密排列的,所以一个ArtMethod的大小,不就是相邻两个ArtMethod的起始地址的差值吗?
正是如此,我们就从这个排列特点入手,自己构造一个类,以一种巧妙的方式获取到这个差值:
public class NativeStructsModel {
final public static void f1() {}
final public static void f2() {}
}
由于 f1和f2都是static方法,所以都属于 direct ArtMethod Array。由于 NativeStructsModel类中只存这两个方法,因此它们肯定是相邻的。
那么就可以在JNI层取得它们的地址差值:
size_t firMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, “f1”, “()V”); // 计算出 f1()的地址
size_t secMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, “f2”, “()V”); // 计算出 f2()的地址
文末
初级工程师拿到需求会直接开始做,然后做着做着发现有问题了,要么技术实现不了,要么逻辑有问题。
而高级工程师拿到需求会考虑很多,技术的可行性?对现有业务有没有帮助?对现有技术架构的影响?扩展性如何?等等…之后才会再进行设计编码阶段。
而现在随着跨平台开发,混合式开发,前端开发之类的热门,Android开发者需要学习和掌握的技术也在不断的增加。
通过和一些行业里的朋友交流讨论,以及参考现在大厂面试的要求。我们花了差不多一个月时间整理出了这份Android高级工程师需要掌握的所有知识体系。你可以看下掌握了多少。
混合式开发,微信小程序。都是得学会并且熟练的
这些是Android相关技术的内核,还有Java进阶
高级进阶必备的一些技术。像移动开发架构项目实战等
Android前沿技术;包括了组件化,热升级和热修复,以及各种架构跟框架的详细技术体系
以上即是我们整理的Android高级工程师需要掌握的技术体系了。可能很多朋友觉得很多技术自己都会了,只是一些新的技术不清楚而已。应该没什么太大的问题。
而这恰恰是问题所在!为什么别人高级工程师能年限突破30万,而你只有十几万呢?
就因为你只需补充你自己认为需要的,但并不知道企业需要的。这个就特别容易造成差距。因为你的技术体系并不系统,是零碎的,散乱的。那么你凭什么突破30万年薪呢?
我这些话比较直接,可能会戳到一些人的玻璃心,但是我知道肯定会对一些人起到点醒的效果的。而但凡只要有人因为我的这份高级系统大纲以及这些话找到了方向,并且付出行动去提升自我,为了成功变得更加努力。那么我做的这些就都有了意义。
喜欢的话请帮忙转发点赞一下能让更多有需要的人看到吧。谢谢!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
Android前沿技术;包括了组件化,热升级和热修复,以及各种架构跟框架的详细技术体系
[外链图片转存中…(img-opDPRREl-1714665807308)]
以上即是我们整理的Android高级工程师需要掌握的技术体系了。可能很多朋友觉得很多技术自己都会了,只是一些新的技术不清楚而已。应该没什么太大的问题。
而这恰恰是问题所在!为什么别人高级工程师能年限突破30万,而你只有十几万呢?
就因为你只需补充你自己认为需要的,但并不知道企业需要的。这个就特别容易造成差距。因为你的技术体系并不系统,是零碎的,散乱的。那么你凭什么突破30万年薪呢?
我这些话比较直接,可能会戳到一些人的玻璃心,但是我知道肯定会对一些人起到点醒的效果的。而但凡只要有人因为我的这份高级系统大纲以及这些话找到了方向,并且付出行动去提升自我,为了成功变得更加努力。那么我做的这些就都有了意义。
喜欢的话请帮忙转发点赞一下能让更多有需要的人看到吧。谢谢!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!