学习福利
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
…
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()的地址
size_t methSize = secMid - firMid; //因为f2和f1是相邻的,所以 两者相减就是一个 ArtMethod的大小
memcpy(smeth, dmeth, methSize); //将size代入,完成替换
**值得一提的是,由于忽略了底层ArtMethod的结构差异,对于所有Android版本都不需要区分,而统一以 memcpy()
实现即可,代码量大大减少。
即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就无须再做适配。**
(1)方法调用时的权限检查
看到这里,你可能会产生疑惑:我们只是替换了ArtMethod的内容,但替换方案的所属类,和原有方法的所属类,是不同的类型,被替换的方法有权限访问这个类的其他private方法吗?
以这段简单的代码为例子:
public class Demo {
Demo() {
func();
}
private void func() {
}
}
假如我们想要替换func()
方法,会不会因为它是private的关系,而在构造函数中访问不了。
来看看 Demo()
这个构造函数的Dex Code和Native Code,看看它是怎么调用 func()
d的
void com.rikkatheworld.demo.Demo.() (dex_method_idx=20628)
DEX CODE:
… …
0x0003: 7010 9550 0000 | invoke-direct {v0}, void com.rikkatheworld.demo.Demo.func() //method@20629
… …
CODE: (code_offset=0x006fd86c size_offset=0x006fd868 size=150)…
… …
0x006fd8c4: f94003e0 ldr x0, [sp] ;x0 = 的ArtMethod*
0x006fd8c8: b9400400 ldr w0, [x0, #4] ;w0 = dex_cache_resolved_methods_
0x006fd8cc: d2909710 mov x16, #0x84b8 ;x16 = 0x84b8
0x006fd8d0: f2a00050 movk x16, #0x2, lsl #16 ;x16 = 0x84b8 + 0x20000 = 0x284b8 = (20629 + 2) * 8,
;也就是Demo.func的 ArtMethod* 相对于表头dex_cache_resolved_method_的偏移
0x006fd8d4: f8706800 ldr x0, [x0, x16] ;得到Demo.fun()的ArtMethod*
0x006fd8d8: f940181e ldr lr, [x0, #48] ;取得其entry_point_from_quick_compiled_code_
0x006fd8dc: d63f03c0 blr lr ;跳转执行
这个调用逻辑和之前Activity的例子大同小异,需要注意的地方是,在构造函数调用同一个类的私有方法func()
时,没有做任何权限检查。
也就是说,这时即使把 func()
偷梁换柱,也能直接跳过去正常执行而不会报错。
可以推测,在 dex2oat生成AOT机器码时是做一些检查和优化的,由于在dex2oat编译机器码时确认了两个方法同属一个类,所以机器码中就不存在权限检查的相关代码。
(2)同名包下的权限问题
但是,并非所有的方法都可以这么顺利的访问,我们发现补丁中的类在访问同名包下的类时,会报访问权限异常:
Caused by: java.lang.IllegalAccessError:
Method ‘void com.rikkatheworld.demo.BaseBug.test()’ is inaccessible to class ‘com.rikkatheworld…demo.MyClass’ (declaration of ‘com.rikkatheworld.demo.MyClass’ …)
虽然 com.rikkatheworld.demo.BaseBug
和 com.rikkatheworld.demo.MyClass
是同一个包 com.rikkatheworld.demo
下面的,但是由于我们替换了 com.rikkatheworld.demo.BaseBug.test()
,而这个替换了的 BaseBug.test()
是从补丁包的Classloader中加载的,与原有的base包就不是同一个Classloader了,这样就导致两个类无法被判别为同包名。
具体的校验逻辑在虚拟机代码的 Class::IsInSamePackage
中:
// 8.0 art\runtime\mirror\ class.cc
bool Class::IsInSamePackage(ObjPtr that) {
ObjPtr klass1 = this;
ObjPtr klass2 = that;
if (klass1 == klass2) {
return true;
}
if (klass1->GetClassLoader() != klass2->GetClassLoader()) { // 1
return false;
}
while (klass1->IsArrayClass()) {
klass1 = klass1->GetComponentType();
}
while (klass2->IsArrayClass()) {
klass2 = klass2->GetComponentType();
}
if (klass1 == klass2) {
return true;
}
std::string temp1, temp2;
return IsInSamePackage(klass1->GetDescriptor(&temp1), klass2->GetDescriptor(&temp2));
}
尾声
最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
Android进阶学习资料库
一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
[外链图片转存中…(img-IEixh90u-1715416946511)]
Android进阶学习资料库
一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!
[外链图片转存中…(img-tHrJrjHs-1715416946512)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!