mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
del
This commit is contained in:
421
极客时间专栏/geek/Android开发高手课/特别放送/Android JVM TI机制详解(内含福利彩蛋).md
Normal file
421
极客时间专栏/geek/Android开发高手课/特别放送/Android JVM TI机制详解(内含福利彩蛋).md
Normal file
@@ -0,0 +1,421 @@
|
||||
<audio id="audio" title="Android JVM TI机制详解(内含福利彩蛋)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/1e/e9b2621b301ebf777d52b63f75a9441e.mp3"></audio>
|
||||
|
||||
你好,我是孙鹏飞。
|
||||
|
||||
在专栏[卡顿优化](http://time.geekbang.org/column/article/73277)的分析中,绍文提到可以利用JVM TI机制获得更加非常丰富的顿现场信息,包括内存申请、线程创建、类加载、GC信息等。
|
||||
|
||||
JVM TI机制究竟是什么?它为什么如此的强大?怎么样将它应用到我们的工作中?今天我们一起来解开它神秘的面纱。
|
||||
|
||||
## JVM TI介绍
|
||||
|
||||
JVM TI全名是[Java Virtual Machine Tool Interface](https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html#SpecificationIntro),是开发虚拟机监控工具使用的编程接口,它可以监控JVM内部事件的执行,也可以控制JVM的某些行为,可以实现调试、监控、线程分析、覆盖率分析工具等。
|
||||
|
||||
JVM TI属于[Java Platform Debugger Architecture](https://docs.oracle.com/javase/7/docs/technotes/guides/jpda/architecture.html)中的一员,在Debugger Architecture上JVM TI可以算作一个back-end,通过JDWP和front-end JDI去做交互。需要注意的是,Android内的JDWP并不是基于JVM TI开发的。
|
||||
|
||||
从Java SE 5开始,Java平台调试体系就使用JVM TI替代了之前的JVMPI和JVMDI。如果你对这部分背景还不熟悉,强烈推荐先阅读下面这几篇文章:
|
||||
|
||||
<li>
|
||||
[深入 Java 调试体系:第 1 部分,JPDA 体系概览](https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/index.html)
|
||||
</li>
|
||||
<li>
|
||||
[深入 Java 调试体系:第 2 部分,JVMTI 和 Agent 实现](https://www.ibm.com/developerworks/cn/java/j-lo-jpda2/index.html)
|
||||
</li>
|
||||
<li>
|
||||
[深入 Java 调试体系:第 3 部分,JDWP 协议及实现](https://www.ibm.com/developerworks/cn/java/j-lo-jpda3/index.html)
|
||||
</li>
|
||||
<li>
|
||||
[深入 Java 调试体系:第 4 部分,Java 调试接口(JDI)](https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html)
|
||||
</li>
|
||||
|
||||
虽然Java已经使用了JVM TI很多年,但从源码上看在Android 8.0才[集成](http://androidxref.com/8.0.0_r4/xref/art/runtime/openjdkjvmti/)了JVM TI v1.2,主要是需要在Runtime中支持修改内存中的Dex和监控全局的事件。有了JVM TI的支持,我们可以实现很多调试工具没有实现的功能,或者定制我们自己的Debug工具来获取我们关心的数据。
|
||||
|
||||
现阶段已经有工具使用JVM TI技术,比如Android Studio的Profilo工具和Linkedin的[dexmaker-mockito-inline](https://github.com/linkedin/dexmaker/tree/master/dexmaker-mockito-inline)工具。Android Studio使用JVM TI机制实现了实时的内存监控,对象分配切片、GC事件、Memory Alloc Diff功能,非常强大;dexmaker使用该机制实现Mock final methods和static methods。
|
||||
|
||||
**1. JVM TI支持的功能**
|
||||
|
||||
在介绍JVM TI的实现原理之前,我们先来看一下JVM TI提供了什么功能?我们可以利用这些功能做些什么?
|
||||
|
||||
**线程相关事件 -> 监控线程创建堆栈、锁信息**
|
||||
|
||||
<li>
|
||||
ThreadStart :线程在执行方法前产生线程启动事件。
|
||||
</li>
|
||||
<li>
|
||||
ThreadEnd:线程结束事件。
|
||||
</li>
|
||||
<li>
|
||||
MonitorWait:wait方法调用后。
|
||||
</li>
|
||||
<li>
|
||||
MonitorWaited:wait方法完成等待。
|
||||
</li>
|
||||
<li>
|
||||
MonitorContendedEnter:当线程试图获取一个已经被其他线程持有的对象锁时。
|
||||
</li>
|
||||
<li>
|
||||
MonitorContendedEntered:当线程获取到对象锁继续执行时。
|
||||
</li>
|
||||
|
||||
**类加载准备事件 -> 监控类加载**
|
||||
|
||||
<li>
|
||||
ClassFileLoadHook:在类加载之前触发。
|
||||
</li>
|
||||
<li>
|
||||
ClassLoad:某个类首次被加载。
|
||||
</li>
|
||||
<li>
|
||||
ClassPrepare:某个类的准备阶段完成。
|
||||
</li>
|
||||
|
||||
**异常事件 -> 监控异常信息**
|
||||
|
||||
<li>
|
||||
Exception:有异常抛出的时候。
|
||||
</li>
|
||||
<li>
|
||||
ExceptionCatch:当捕获到一个异常时候。
|
||||
</li>
|
||||
|
||||
**调试相关**
|
||||
|
||||
<li>
|
||||
SingleStep:步进事件,可以实现相当细粒度的字节码执行序列,这个功能可以探查多线程下的字节码执行序列。
|
||||
</li>
|
||||
<li>
|
||||
Breakpoint:当线程执行到一个带断点的位置,断点可以通过JVMTI SetBreakpoint方法来设置。
|
||||
</li>
|
||||
|
||||
**方法执行**
|
||||
|
||||
<li>
|
||||
FramePop:当方法执行到retrun指令或者出现异常时候产生,手动调用NofityFramePop JVM TI函数也可产生该事件。
|
||||
</li>
|
||||
<li>
|
||||
MethodEntry:当开始执行一个Java方法的时候。
|
||||
</li>
|
||||
<li>
|
||||
MethodExit:当方法执行完成后,产生异常退出时。
|
||||
</li>
|
||||
<li>
|
||||
FieldAccess:当访问了设置了观察点的属性时产生事件,观察点使用SetFieldAccessWatch函数设置。
|
||||
</li>
|
||||
<li>
|
||||
FieldModification:当设置了观察点的属性值被修改后,观察点使用SetFieldModificationWatch设置。
|
||||
</li>
|
||||
|
||||
**GC -> 监控GC事件与时间**
|
||||
|
||||
<li>
|
||||
GarbageCollectionStart:GC启动时。
|
||||
</li>
|
||||
<li>
|
||||
GarbageCollectionFinish:GC结束后。
|
||||
</li>
|
||||
|
||||
**对象事件 -> 监控内存分配**
|
||||
|
||||
<li>
|
||||
ObjectFree:GC释放一个对象时。
|
||||
</li>
|
||||
<li>
|
||||
VMObjectAlloc:虚拟机分配一个对象的时候。
|
||||
</li>
|
||||
|
||||
**其他**
|
||||
|
||||
- NativeMethodBind:在首次调用本地方法时或者调用JNI RegisterNatives的时候产生该事件,通过该回调可以将一个JNI调用切换到指定的方法上。
|
||||
|
||||
通过上面的事件描述可以大概了解到JVM TI支持什么功能,详细的回调函数参数可以从JVM TI[规范文档](https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html)里获取到,**我们可以通过这些功能实们定制的性能监控、数据采集、行为修改等工具。**
|
||||
|
||||
**2. JVM TI实现原理**
|
||||
|
||||
JVM TI Agent的启动需要虚拟机的支持,我们的Agent和虚拟机运行在同一个进程中,虚拟机通过dlopen打开我们的Agent动态链接库,然后通过Agent_OnAttach方法来调用我们定义的初始化逻辑。
|
||||
|
||||
JVM TI的原理其实很简单,以VmObjectAlloc事件为例,当我们通过SetEventNotificationMode函数设置JVMTI_EVENT_VM_OBJECT_ALLOC回调的时候,最终会调用到art::Runtime::Current() -> GetHeap() -> SetAllocationListener(listener);
|
||||
|
||||
在这个方法中,listener是JVM TI实现的一个虚拟机提供的art::gc::AllocationListener回调,当虚拟机分配对象内存的时候会调用该回调,源码可见[heap-inl.h#194](http://androidxref.com/9.0.0_r3/xref/art/runtime/gc/heap-inl.h#194),同时在该回调函数里也会调用我们之前设置的callback方法,这样事件和相关的数据就会透传到我们的Agent里,来实现完成事件的监听。
|
||||
|
||||
类似atrace和StrictMode,JVM TI的每个事件都需要在源码中埋点支持。感兴趣的同学,可以挑选一些事件在源码中进一步跟踪。
|
||||
|
||||
## JVM TI Agent开发
|
||||
|
||||
JVM TI Agent程序使用C/C++语言开发,也可以使用其他支持C语言调用语言开发,比如Rust。
|
||||
|
||||
JVM TI所涉及的常量、函数、事件、数据类型都定义在jvmti.h文件中,我们需要下载该文件到项目中引用使用,你可以从Android项目里下载它的[头文件](http://androidxref.com/9.0.0_r3/xref/art/openjdkjvmti/include/)。
|
||||
|
||||
JVM TI Agent的产出是一个so文件,在Android里通过系统提供的[Debug.attachJvmtiAgent](https://developer.android.com/reference/kotlin/android/os/Debug#attachJvmtiAgent%28kotlin.String%2C+kotlin.String%2C+java.lang.ClassLoader%29)方法来启动一个JVM TI Agent程序。
|
||||
|
||||
```
|
||||
static fun attachJvmtiAgent(library: String, options: String?, classLoader: ClassLoader?): Unit
|
||||
|
||||
```
|
||||
|
||||
library是so文件的绝对地址。需要注意的是API Level为28,而且需要应用开启了[android:debuggable](https://developer.android.com/guide/topics/manifest/application-element#debug)才可以使用,**不过我们可以通过强制开启debug来在release版里启动JVM TI功能**。
|
||||
|
||||
Android下的JVM TI Agent在被虚拟机加载后会及时调用Agent_OnAttach方法,这个方法可以当作是Agent程序的main函数,所以我们需要在程序里实现下面的函数。
|
||||
|
||||
```
|
||||
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options,void *reserved)
|
||||
|
||||
```
|
||||
|
||||
你可以在这个方法里进行初始化操作。
|
||||
|
||||
通过JavaVM::GetEnv函数拿到jvmtiEnv*环境指针(Environment Pointer),通过该指针可以访问JVM TI提供的函数。
|
||||
|
||||
```
|
||||
jvmtiEnv *jvmti_env;jint result = vm->GetEnv((void **) &jvmti_env, JVMTI_VERSION_1_2);
|
||||
|
||||
```
|
||||
|
||||
通过AddCapabilities函数来开启需要的功能,也可以通过下面的方法开启所有的功能,不过开启所有的功能对虚拟机的性能有所影响。
|
||||
|
||||
```
|
||||
void SetAllCapabilities(jvmtiEnv *jvmti) {
|
||||
jvmtiCapabilities caps;
|
||||
jvmtiError error;
|
||||
error = jvmti->GetPotentialCapabilities(&caps);
|
||||
error = jvmti->AddCapabilities(&caps);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
GetPotentialCapabilities函数可以获取当前环境支持的功能集合,通过jvmtiCapabilities结构体返回,该结构体里标明了支持的所有功能,可以通过[jvmti.h](http://androidxref.com/9.0.0_r3/xref/art/openjdkjvmti/include/jvmti.h#712)来查看,大概内容如下。
|
||||
|
||||
```
|
||||
typedef struct {
|
||||
unsigned int can_tag_objects : 1;
|
||||
unsigned int can_generate_field_modification_events : 1;
|
||||
unsigned int can_generate_field_access_events : 1;
|
||||
unsigned int can_get_bytecodes : 1;
|
||||
unsigned int can_get_synthetic_attribute : 1;
|
||||
unsigned int can_get_owned_monitor_info : 1;
|
||||
......
|
||||
} jvmtiCapabilities;
|
||||
|
||||
```
|
||||
|
||||
然后通过AddCapabilities方法来启动需要的功能,如果需要单独添加功能,则可以通过如下方法。
|
||||
|
||||
```
|
||||
jvmtiCapabilities caps;
|
||||
memset(&caps, 0, sizeof(caps));
|
||||
caps.can_tag_objects = 1;
|
||||
|
||||
```
|
||||
|
||||
到此JVM TI的初始化操作就已经完成了。
|
||||
|
||||
所有的函数和数据结构类型说明可以在[这里](https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html)找到。下面我来介绍一些常用的功能和函数。
|
||||
|
||||
**1. JVM TI事件监控**
|
||||
|
||||
JVM TI的一大功能就是可以收到虚拟机执行时候的各种事件通知。
|
||||
|
||||
首先通过SetEventCallbacks方法来设置目标事件的回调函数,如果callbacks传入nullptr则清除掉所有的回调函数。
|
||||
|
||||
```
|
||||
jvmtiEventCallbacks callbacks;
|
||||
memset(&callbacks, 0, sizeof(callbacks));
|
||||
|
||||
callbacks.GarbageCollectionStart = &GCStartCallback;
|
||||
callbacks.GarbageCollectionFinish = &GCFinishCallback;
|
||||
int error = jvmti_env->SetEventCallbacks(&callbacks, sizeof(callbacks));
|
||||
|
||||
```
|
||||
|
||||
设置了回调函数后,如果要收到目标事件的话需要通过SetEventNotificationMode,这个函数有个需要注意的地方是event_thread,如果参数event_thread参数为nullptr,则会全局启用改目标事件回调,否则只在指定的线程内生效,比如很多时候对于一些事件我们只关心主线程。
|
||||
|
||||
```
|
||||
jvmtiError SetEventNotificationMode(jvmtiEventMode mode,
|
||||
jvmtiEvent event_type,
|
||||
jthread event_thread,
|
||||
...);
|
||||
typedef enum {
|
||||
JVMTI_ENABLE = 1,//开启
|
||||
JVMTI_DISABLE = 0 .//关闭
|
||||
} jvmtiEventMode;
|
||||
|
||||
```
|
||||
|
||||
以上面的GC事件为例,上面设置了GC事件的回调函数,如果想要在回调方法里接收到事件则需要使用SetEventNotificationMode开启事件,需要说明的是SetEventNotificationMode和SetEventCallbacks方法调用没有先后顺序。
|
||||
|
||||
```
|
||||
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_START, nullptr);
|
||||
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_FINISH, nullptr);
|
||||
|
||||
```
|
||||
|
||||
通过上面的步骤就可以在虚拟机产生GC事件后在回调函数里获取到对应的函数了,这个Sample需要注意的是在gc callback里禁止使用JNI和JVM TI函数,因为虚拟机处于停止状态。
|
||||
|
||||
```
|
||||
void GCStartCallback(jvmtiEnv *jvmti) {
|
||||
LOGI("==========触发 GCStart=======");
|
||||
}
|
||||
|
||||
void GCFinishCallback(jvmtiEnv *jvmti) {
|
||||
LOGI("==========触发 GCFinish=======");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Sample效果如下。
|
||||
|
||||
```
|
||||
com.dodola.jvmti I/jvmti: ==========触发 GCStart=======
|
||||
com.dodola.jvmti I/jvmti: ==========触发 GCFinish=======
|
||||
|
||||
```
|
||||
|
||||
**2. JVM TI字节码增强**
|
||||
|
||||
JVM TI可以在虚拟机运行的状态下对字节码进行修改,可以通过下面三种方式修改字节码。
|
||||
|
||||
<li>
|
||||
Static:在虚拟机加载Class文件之前,对字节码修改。该方式一般不采用。
|
||||
</li>
|
||||
<li>
|
||||
Load-Time:在虚拟机加载某个Class时,可以通过JVM TI回调拿到该类的字节码,会触发ClassFileLoadHook回调函数,该方法由于ClassLoader机制只会触发一次,由于我们Attach Agent的时候经常是在虚拟机执行一段时间之后,所以并不能修改已经加载的Class比如Object,所以需要根据Class的加载时机选择该方法。
|
||||
</li>
|
||||
<li>
|
||||
Dynamic:对于已经载入的Class文件也可以通过JVM TI机制修改,当系统调用函数RetransformClasses时会触发ClassFileLoadHook,此时可以对字节码进行修改,该方法最为实用。
|
||||
</li>
|
||||
|
||||
传统的JVM操作的是Java Bytecode,Android里的字节码操作的是[Dalvik Bytecode](https://source.android.com/devices/tech/dalvik/dalvik-bytecode),Dalvik Bytecode是寄存器实现的,操作起来相对JavaBytecode来说要相对容易一些,可以不用处理本地变量和操作数栈的交互。
|
||||
|
||||
使用这个功能需要开启JVM TI字节码增强功能。
|
||||
|
||||
```
|
||||
jvmtiCapabilities.can_generate_all_class_hook_events=1 //开启 class hook 功能标记
|
||||
jvmtiCapabilities.can_retransform_any_class=1 //开启对任意类进行 retransform 操作
|
||||
|
||||
```
|
||||
|
||||
然后注册ClassFileLoadHook事件回调。
|
||||
|
||||
```
|
||||
jvmtiEventCallbacks callbacks;s
|
||||
callbacks.ClassFileLoadHook = &ClassTransform;
|
||||
|
||||
```
|
||||
|
||||
这里说明一下ClassFileLoadHook的函数原型,后面会讲解如何重新修改现有字节码。
|
||||
|
||||
```
|
||||
static void ClassTransform(
|
||||
jvmtiEnv *jvmti_env,//jvmtiEnv 环境指针
|
||||
JNIEnv *env,//jniEnv 环境指针
|
||||
jclass classBeingRedefined,//被重新定义的class 信息
|
||||
jobject loader,//加载该 class 的 classloader,如果该项为 nullptr 则说明是 BootClassLoader 加载的
|
||||
const char *name,//目标类的限定名
|
||||
jobject protectionDomain,//载入类的保护域
|
||||
jint classDataLen,//class 字节码的长度
|
||||
const unsigned char *classData,//class 字节码的数据
|
||||
jint *newClassDataLen,//新的类数据的长度
|
||||
unsigned char **newClassData) //新类的字节码数据
|
||||
|
||||
```
|
||||
|
||||
然后开启事件,完整的初始化逻辑可参考Sample中的代码。
|
||||
|
||||
```
|
||||
SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL)
|
||||
|
||||
```
|
||||
|
||||
下面以Sample代码作为示例来讲解如何在Activity类的onCreate方法中插入一行日志调用代码。
|
||||
|
||||
通过上面的步骤后就可以在虚拟机第一次加载类的时候和在调用RetransformClasses或者RedefineClasses时,在ClassFileLoadHook回调方法里会接收到事件回调。我们目标类是Activity,它在启动应用的时候就已经触发了类加载的过程,由于这个Sample开启事件的时机很靠后,所以此时并不会收到加载Activity类的事件回调,所以需要调用RetransformClasses来触发事件回调,这个方法用于对已经载入的类进行修改,传入一个要修改类的Class数组和数组长度。
|
||||
|
||||
```
|
||||
jvmtiError RetransformClasses(jint class_count, const jclass* classes)
|
||||
|
||||
```
|
||||
|
||||
调用该方法后会在ClassFileLoadHook设置的回调,也就是上面的ClassTran sform方法中接收到回调,在这个回调方法中我们通过字节码处理工具来修改原始类的字节码。
|
||||
|
||||
类的修改会触发虚拟机使用新的方法,旧的方法将不再被调用,如果有一个方法正在栈帧上,则这个方法会继续运行旧的方法的字节码。RetransformClasses 的修改不会导致类的初始化,也就是不会重新调用<cinit>方法,类的静态变量的值和实例变量的值不会产生变化,但目标类的断点会失效。</cinit>
|
||||
|
||||
处理类有一些限制,我们可以改变方法的实现和属性,但不能添加删除重命名方法,不能改变方法签名、参数、修饰符,不能改变类的继承关系,如果产生上面的行为会导致修改失败。修改之后会触发类的校验,而且如果虚拟机里有多个相同的Class ,我们需要注意一下取到的Class需要是当前生效的Class,按照ClassLoader加载机制也就是说优先使用提前加载的类。
|
||||
|
||||
Sample中实现的效果是在Activity.onCreate方法中增加一行日志输出。
|
||||
|
||||
修改前:
|
||||
|
||||
```
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
.......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
修改后:
|
||||
|
||||
```
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
com.dodola.jvmtilib.JVMTIHelper.printEnter(this,"....");
|
||||
....
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我使用的Dalvik字节码修改库是Android系统源码里提供的一套修改框架[dexter](http://androidxref.com/9.0.0_r3/xref/tools/dexter/),虽然使用起来十分灵活但比较繁琐,也可以使用[dexmaker](https://github.com/linkedin/dexmaker)框架来实现。本例还是使用dexter,框架使用C++开发,可以直接读取classdata然后进行操作,可以类比到ASM框架。下面的代码是核心的操作代码,完整的代码参考本期Sample。
|
||||
|
||||
```
|
||||
ir::Type* stringT = b.GetType("Ljava/lang/String;");
|
||||
ir::Type* jvmtiHelperT=b.GetType("Lcom/dodola/jvmtilib/JVMTIHelper;");
|
||||
lir::Instruction *fi = *(c.instructions.begin());
|
||||
VReg* v0 = c.Alloc<VReg>(0);
|
||||
addInstr(c, fi, OP_CONST_STRING,
|
||||
{v0, c.Alloc<String>(methodDesc, methodDesc->orig_index)});
|
||||
addCall(b, c, fi, OP_INVOKE_STATIC, jvmtiHelperT, "printEnter", voidT, {stringT}, {0});
|
||||
c.Assemble();
|
||||
|
||||
```
|
||||
|
||||
必须通过JVM TI函数Allocate为要修改的类数据分配内存,将new_class_data指向修改后的类bytecode数组,将new_class_data_len置为修改后的类bytecode数组的长度。若是不修改类文件,则不设置new_class_data即可。若是加载了多个JVM TI Agent都启用了该事件,则设置的new_class_data会成为下一个JVM TI Agent的class_data。
|
||||
|
||||
此时我们生成的onCreate方法里已经加上了我们添加的日志方法调用。开启新的Activity会使用新的类字节码执行,同时会使用ClassLoader加载我们注入的com.dodola.jvmtilib.JVMTIHelper类。我在前面说过,Activity是使用BootClassLoader进行加载的,然而我们的类明显不在BootClassLoader里,此时就会产生Crash。
|
||||
|
||||
```
|
||||
java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available
|
||||
|
||||
```
|
||||
|
||||
所以需要想办法将JVMTIHelper类添加到BootClassLoader里,这里可以使用JVM TI提供的AddToBootstrapClassLoaderSearch方法来添加Dex或者APK到Class搜索目录里。Sample里是将 getPackageCodePath添加进去就可以了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我主要讲解了JVM TI的概念和原理,以及它可以实现的功能。通过JVM TI可以完成很多平时可能需要很多“黑科技”才可以获取到的数据,比如[Thread Park Start/Finish](https://android-review.googlesource.com/c/platform/art/+/822440)事件、获取一个锁的waiters等。
|
||||
|
||||
可能在Android圈里了解JVM TI的人不多,对它的研究还没有非常深入。目前JVM TI的功能已经十分强大,后续的Android版本也会进一步增加更多的功能支持,这样它可以做的事情将会越来越多。我相信在未来,它将会是本地自动化测试,甚至是线上远程诊断的一大“杀器”。
|
||||
|
||||
在本期的[Sample](https://github.com/AndroidAdvanceWithGeektime/JVMTI_Sample)里,我们提供了一些简单的用法,你可以在这个基础之上完成扩展,实现自己想要的功能。
|
||||
|
||||
## 相关资料
|
||||
|
||||
1.[深入 Java 调试体系:第 1 部分,JPDA 体系概览](https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/index.html)
|
||||
|
||||
2.[深入 Java 调试体系:第 2 部分,JVMTI 和 Agent 实现](https://www.ibm.com/developerworks/cn/java/j-lo-jpda2/index.html)
|
||||
|
||||
3.[深入 Java 调试体系:第 3 部分,JDWP 协议及实现](https://www.ibm.com/developerworks/cn/java/j-lo-jpda3/index.html)
|
||||
|
||||
4.[深入 Java 调试体系:第 4 部分,Java 调试接口(JDI)](https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html)
|
||||
|
||||
5.JVM TI官方文档:[https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html](https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html)
|
||||
|
||||
6.源码是最好的资料:[http://androidxref.com/9.0.0_r3/xref/art/openjdkjvmti/](http://androidxref.com/9.0.0_r3/xref/art/openjdkjvmti/)
|
||||
|
||||
## 福利彩蛋
|
||||
|
||||
根据专栏导读里我们约定的,我和绍文会选出一些认真提交作业完成练习的同学,送出一份“学习加油礼包”。专栏更新到现在,很多同学留下了自己的思考和总结,我们选出了@Owen、@志伟、@许圣明、@小洁、@SunnyBird,送出“[极客时间周历](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2fwl2bk2x20js)”一份,希望更多同学可以加入到学习和讨论中来,与我们一起进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/ce/c91eaa4425b74b8c5d8a044e0332f8ce.png" alt=""><br>
|
||||
@Owen学习总结:[https://github.com/devzhan/Breakpad](https://github.com/devzhan/Breakpad)
|
||||
|
||||
@许圣明、@小洁、@SunnyBird 通过Pull Requests提交了练习作业[https://github.com/AndroidAdvanceWithGeektime/Chapter04/pulls](https://github.com/AndroidAdvanceWithGeektime/Chapter04/pulls)。
|
||||
|
||||
极客时间小助手会在24小时内与获奖用户取得联系,注意查看短信哦~
|
||||
|
||||
|
||||
47
极客时间专栏/geek/Android开发高手课/特别放送/Android工程师的“面试指南”.md
Normal file
47
极客时间专栏/geek/Android开发高手课/特别放送/Android工程师的“面试指南”.md
Normal file
@@ -0,0 +1,47 @@
|
||||
<audio id="audio" title="Android工程师的“面试指南”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1d/db/1db43cbf74cae275a50edf18a44d42db.mp3"></audio>
|
||||
|
||||
你好,我是孙鹏飞。又到了传统的“金三银四”换工作的高峰期,在互联网寒冬下,抓住机会就显得尤为重要了。那作为Android工程师我们应该从哪些方面去准备呢?例如,不太熟悉的技能要不要写在简历上、要复习哪些Android组件的知识、刷算法题目有没有用,可能在面试前你都会仔细考虑这些问题。下面我就结合自身的经验和理解,帮你梳理一下关于简历、面试和算法方面需要准备的内容,分享一些我的心得体会。
|
||||
|
||||
## 简历
|
||||
|
||||
简历在面试过程会起到至关重要的作用,我们需要非常注意简历的撰写。
|
||||
|
||||
在面试的过程中,面试官通常会非常关注你简历中的工作经历、项目介绍、技能特长这三部分的内容,如果你面试的公司没有固定题目的话,那很多问题都会围绕你简历里这三部分内容去问。这里需要注意的一点是相关技能的书写,首先你要让面试官明确你面试的定级是什么。很多时候一个职位对应了很多个职级,在投简历的时候,你的简历需要让面试官给你一个比较明确的定级,否则面试过程会比较被动,也会影响面试官对你的判断。因此这部分的内容需要突出自己的特长,也要写一些现在公司相对关心的问题,比如你对插件化、热修复、组件化、性能优化等很熟悉,就可以明确的写上,但如果不是很熟悉那么尽量不要去写。如果你对Android某部分内容很熟悉就可以写得相对详细一些,比如你对Handler、Binder机制很熟悉,就可以写“熟悉Android常见机制,比如Handler、Binder机制等”。而看到你很熟悉这部分内容,面试官可能在问问题时一层层深入,因此你肯定需要提前准备一下这部分内容如何讲解,基本可以从机制的优点、重点、难点三方面去说明。
|
||||
|
||||
关于项目介绍主要体现你在这个项目或者这个团队中的作用,突出自己的贡献和项目的难点。很多同学可能在公司一直做需求的开发,会觉得自己的项目经验没有亮点,难度也没有那么大,会觉得在这部分内容上比较吃亏。其实每个需求下来的时候你肯定会对这个需求有一个自己的设计,在这个过程中你会考虑如何对现有代码的影响最小,如何快捷清晰的实现功能,以及在开发过程中对组件、控件的封装,考虑如何优化性能,有没有新的技术可以帮助开发这个需求的…我想我们每个需求都是通过一系列的考量和设计后才实施的,你可以回去翻一下自己的代码,然后考虑一下如何把你的这些设计和思考体现在简历上,同样也是个不错的说明。
|
||||
|
||||
## 面试
|
||||
|
||||
对于Android工程师来说,面试开始的时候都会问一些算法和Android、Java的基础知识。针对Java的基础知识,我建议你看一下《码出高效:Java 开发手册》《深入理解Java虚拟机》《Java并发编程的艺术》这三本书。对于Android的面试题,大多都是跟系统原理有关的内容,但也有很多没有准确答案的问题,比如四大组件的原理这样的题目,需要你从一个宏观的角度去解释一下四大组件,或者你也可以拆分开一个个去讲解。
|
||||
|
||||
在面试前你需要提前准备一下,避免回答问题的时候没有条理,导致面试官对你的逻辑思维能力和语言表达能力产生不好的判断。一些Android经常使用到的组件一定要理解清楚,比如Handler.postDelay的机制、触摸事件机制、自定义View、如何计算View大小、容器控件如何对子控件进行布局、数据库基本操作、Binder机制、LMK机制等。还有面试官也可能会问一些开源框架的原理,我建议你也要多了解一些优秀的网络框架、图片加载框架、日志记录框架、EventBus、AAC框架的原理。对于相对复杂的插件化和热修复来说,热修复可以去看一下《深入探索Android热修复》这本书,插件化可以去看[《Android插件化原理解析》](http://weishu.me/2016/01/28/understand-plugin-framework-overview/)这个系列的文章。还有性能优化,最近几年公司对性能优化关注很多,有的同学可能做过专门的性能优化或者自己开发过一些工具总结过一些方法论,这样比较好答一些。但是大部分同学可能平时都在关注业务需求开发,性能优化的实战可能并不是很多。我建议你可以从业务开发过程中找一些点来说,比如在做一些公共的业务组件时需要在启动时初始化,那么就需要注意初始化过程中的性能;又或者在做一个列表页面的时候,在复杂的列表View下如何保证滑动性能。相信你在平时开发过程中都会有自己的思考,可以结合具体的情景讲出来。
|
||||
|
||||
面试的后面大多都会从项目入手,你需要在面试之前针对你的项目做详细的准备。比如面试官会让你介绍一下你的项目,你需要体现出这个项目的难点、你在项目中的贡献、项目的具体实现等,有可能还会问到一些具体的细节,所以建议是实事求是去讲,但一定要对自己的模块非常清晰。除了技术面试以外,有时还有可能会考察一些软技能,比如面试官会考察你跨部门协作能力、沟通能力、时间管理、任务分配和职业规划等。
|
||||
|
||||
面试更多的还是要靠平时的积累和临场的发挥,做总结是很重要的,因为很多内容不经常使用的话过一段时间之后就会忘掉,这样就会出现原本自己做过的东西,因为忘记了细节,结果在面试过程中没法很好地展现出来。就比如插件化、热修复这样的技术,其实原理相对来说比较简单,但是在开发的过程中会遇到很多很多的坑,如果没有一些关键点的文字记录,过一段时间之后可能就忘记了某段代码是用于什么目的。所以在做完一次需求之后尽量多总结项目中的难点,使用到的框架以及这个框架的原理,以及其中花费时间最长的地方。另外Bug最多的地方也要做总结一下原因,这样在面试前就不用把代码再翻一遍,了解自己的项目细节就可以做到游刃有余了。
|
||||
|
||||
对于复习,首先要对自己做一次自我了解,我是通过画脑图来进行这个过程的,我会整体默想一遍大概的知识体系,画成类似下图。回想每个知识点可能考到的内容,记录下自己模糊的地方,然后去看网上同学们总结的面试题,再对每个题目都做一下回答。这是一个迭代过程。在你预想的问题都可以回答上来的时候,就需要深入挖掘一下技术细节和深度了,比如我工作中开发了一个PLT Hook工具,这个工具可能是我参考开源项目并封装修改过来的,但对其中的细节并没有很了解,这个时候你就要对这个开源项目所涉及的内容做一次系统学习了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f0/1e/f040ca609fe4785bf0ed2932b4bfa01e.png" alt="">
|
||||
|
||||
另一方面是需要在面试过程中提高面试官对自己的级别评价,比如大部分人回答GC的问题都是按照《深入理解Java虚拟机》里的内容复述一遍,这种回答基本也是可以的。不过毕竟Java虚拟机和Android虚拟机的GC还是有些差别的,如果自己阅读过Android虚拟机GC相关资料或者自己分析过源码的话,可以从Android虚拟机的角度解释GC,比如Android虚拟机里MarkSweep算法的增量回收、并行回收等,后台GC和前台GC、VisitRoot的执行过程、GC的触发方式、TLAB的处理、ConcurrentGC的原理、堆的Trim过程、内存碎片的解决、Reference的处理、finalize函数调用等展开讲。如果你对一个机制很熟悉的话,可以把话题引到这上面去,然后一层层对这个知识点深入讲解,这样可以提升面试官对你的等级评价。
|
||||
|
||||
## 算法
|
||||
|
||||
算法是一定要复习的,在很多面试的过程中都会穿插算法题。面试的算法题一般不会很难,可以分为基础的数据结构,比如数组、链表、栈、队列、二叉树、堆的使用,这几种常见的数据结构的基础操作一定要很熟悉,比如链表逆置、删除、获取第K个元素、判断是否有环等,二叉树翻转、深度遍历、层级遍历、求树深度、公共父节点等。另一种是常见的搜索、排序算法,这两类算法出现频率很高,一定要知道它们常见的几种实现方式,比如排序方式有冒泡、快排、插入、归并、堆排序等。注意这里一定不要简单地去记忆算法实现,因为面试的时候可能不会直接让你写出对应的算法,会出一些使用搜索或者排序算法来实现的题目,这类题目你可以去LeetCode上通过标签过滤出来。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/84/38137da67092caa6f164aa4445fe9984.png" alt="">
|
||||
|
||||
另一部分的算法题可能集中在贪心、动态规划、分治算法、深搜广搜等,这一类的算法相对需要一些技巧性。但面试算法题通常不需要太多行代码就能完成,一般都是在几十行内就能完成的,所以你可以优先去找一些经典题目来做,比如爬楼梯、最大子序和等。但也会有一些相对复杂的题目是几种算法结合在一起的,比如二叉树的最大路径和就是深度搜索和动态规划一起使用的题目。除此之外,也可能会遇到通过其他问题引申出的一些算法题目,比如HashMap可能会引申出红黑树的实现等。这些都有可能需要准备,虽然它可能不会成为你整个面试的一个绊脚石,但有可能成为你获取一个高评价的筹码。
|
||||
|
||||
“临时抱佛脚”对于算法的学习和积累作用不是很大,因此需要我们在平时繁忙的工作中抽出一些时间来复习,你也可以去LeetCode、LintCode上刷刷题。另外,虽然大部分面试的算法题目都是LeetCode上的简单题目,但你同样也需要关注一些中等和困难难度的经典题目。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我并没有涉及太多具体的面试题,更多侧重的是如何准备面试,而面试的准备其实是在我们平时工作过程中一点一滴积累的,复习只是作为一种在面试前巩固知识的手段。复习的过程主要是我们对知识点的整理和总结,你可以想一下在面试的时候可能会遇到的问题,以及该如何去表达。但是我想说,虽然“临时抱佛脚”的准备可能有时有用,但是在短时间内靠“突击”是很难理解到某个知识点更加深度层次的内容,而且知识面的广度也是需要时间和经验去积累的。所以不管你是否需要面试,在平时工作过程中都需要多思考、多训练、多总结,在有需要的时候可以厚积薄发。
|
||||
|
||||
最后,如果你也在准备面试,可以在留言区分享一下你的准备情况和面试心得。当然你也可以留下你遇到的面试问题,把它分享给其他同学。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
154
极客时间专栏/geek/Android开发高手课/特别放送/Native下如何获取调用栈?.md
Normal file
154
极客时间专栏/geek/Android开发高手课/特别放送/Native下如何获取调用栈?.md
Normal file
@@ -0,0 +1,154 @@
|
||||
<audio id="audio" title="Native下如何获取调用栈?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/15/6c87ddee93100bc7b6f330617e4bb215.mp3"></audio>
|
||||
|
||||
你好,我是simsun,曾在微信从事Android开发,也是开源爱好者、Rust语言“铁粉”。应绍文邀请,很高兴可以在“高手课”里和你分享一些编译方面的底层知识。
|
||||
|
||||
当我们在调试Native崩溃或者在做profiling的时候是十分依赖backtrace的,高质量的backtrace可以大大减少我们修复崩溃的时间。但你是否了解系统是如何生成backtrace的呢?今天我们就来探索一下backtrace背后的故事。
|
||||
|
||||
下面是一个常见的Native崩溃。通常崩溃本身并没有任何backtrace信息,可以直接获得的就是当前寄存器的值,但显然backtrace才是能够帮助我们修复Bug的关键。
|
||||
|
||||
```
|
||||
pid: 4637, tid: 4637, name: crasher >>> crasher <<<
|
||||
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
|
||||
Abort message: 'some_file.c:123: some_function: assertion "false" failed'
|
||||
r0 00000000 r1 0000121d r2 00000006 r3 00000008
|
||||
r4 0000121d r5 0000121d r6 ffb44a1c r7 0000010c
|
||||
r8 00000000 r9 00000000 r10 00000000 r11 00000000
|
||||
ip ffb44c20 sp ffb44a08 lr eace2b0b pc eace2b16
|
||||
backtrace:
|
||||
#00 pc 0001cb16 /system/lib/libc.so (abort+57)
|
||||
#01 pc 0001cd8f /system/lib/libc.so (__assert2+22)
|
||||
#02 pc 00001531 /system/bin/crasher (do_action+764)
|
||||
#03 pc 00002301 /system/bin/crasher (main+68)
|
||||
#04 pc 0008a809 /system/lib/libc.so (__libc_init+48)
|
||||
#05 pc 00001097 /system/bin/crasher (_start_main+38)
|
||||
|
||||
```
|
||||
|
||||
在阅读后面的内容之前,你可以先给自己2分钟时间,思考一下系统是如何生成backtrace的呢?我们通常把生成backtrace的过程叫作unwind,unwind看似和我们平时开发并没有什么关系,但其实很多功能都是依赖unwind的。举个例子,比如你要绘制火焰图或者是在崩溃发生时得到backtrace,都需要依赖unwind。
|
||||
|
||||
## 书本中的unwind
|
||||
|
||||
**1. 函数调用过程**
|
||||
|
||||
如果你在大学时期修过汇编原理这门课程,相信你会对下面的内容还有印象。下图就是一个非常标准的函数调用的过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/2a/09cd560f823de783e22af03a5f837f2a.png" alt="">
|
||||
|
||||
<li>
|
||||
首先假设我们处于函数main()并准备调用函数foo(),调用方会按倒序压入参数。此时第一个参数会在调用栈栈顶。
|
||||
</li>
|
||||
<li>
|
||||
调用invoke foo()伪指令,压入当前寄存器EIP的值到栈,然后载入函数foo()的地址到EIP。
|
||||
</li>
|
||||
<li>
|
||||
此时,由于我们已经更改了EIP的值(为foo()的地址),相当于我们已经进入了函数foo()。在执行一个函数之前,编译器都会给每个函数写一段序言(prologue),这里会压入旧的EBP值,并赋予当前EBP和ESP新的值,从而形成新的一个函数栈。
|
||||
</li>
|
||||
<li>
|
||||
下一步就行执行函数foo()本身的代码了。
|
||||
</li>
|
||||
<li>
|
||||
结束执行函数foo() 并准备返回,这里编译器也会给每个函数插入一段尾声(epilogue)用于恢复调用方的ESP和EBP来重建之前函数的栈和恢复寄存器。
|
||||
</li>
|
||||
<li>
|
||||
执行返回指令(ret),被调用函数的尾声(epilogue)已经恢复了EBP和ESP,然后我们可以从被恢复的栈中依次pop出EIP、所有的参数以及被暂存的寄存器的值。
|
||||
</li>
|
||||
|
||||
读到这里,相信如果没有学过汇编原理的同学肯定会有一些懵,我来解释一下上面提到的寄存器缩写的具体含义,上述命名均使用了x86的命名方式。讲这些是希望你对函数调用有一个初步的理解,其中有很多细节在不同体系结构、不同编译器上的行为都有所区别,所以请你放松心情,跟我一起继续向后看。
|
||||
|
||||
>
|
||||
<p>EBP:基址指针寄存器,指向栈帧的底部。<br>
|
||||
在ARM体系结构中,R11(ARM code)或者R7(Thumb code)起到了类似的作用。在ARM64中,此寄存器为X29。<br>
|
||||
ESP:栈指针寄存器,指向栈帧的栈顶 , 在ARM下寄存器为R13。<br>
|
||||
EIP:指令寄存器,存储的是CPU下次要执行的指令的地址,ARM下为PC,寄存器为R15。</p>
|
||||
|
||||
|
||||
**2. 恢复调用帧**
|
||||
|
||||
如果我们把上述过程缩小,站在更高一层视角去看,所有的函数调用栈都会形成调用帧(stack frame),每一个帧中都保存了足够的信息可以恢复调用函数的栈帧。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/45/017b900048f1be70ec870ea0750d2145.png" alt="">
|
||||
|
||||
我们这里忽略掉其他不相关的细节,重点关注一下EBP、ESP和EIP。你可以看到EBP和ESP分别指向执行函数栈的栈底和栈顶。每次函数调用都会保存EBP和EIP用于在返回时恢复函数栈帧。这里所有被保存的EBP就像一个链表指针,不断地指向调用函数的EBP。 这样我们就可以此为线索,十分优雅地恢复整个调用栈。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7a/f8/7ab1f6e1774731ec96b9c68843a73df8.png" alt="">
|
||||
|
||||
这里我们可以用下面的伪代码来恢复调用栈:
|
||||
|
||||
```
|
||||
void debugger::print_backtrace() {
|
||||
auto curr_func = get_func_from_pc(get_pc());
|
||||
output_frame(curr_func);
|
||||
|
||||
auto frame_pointer = get_register_value(m_pid, reg::rbp);
|
||||
auto return_address = read_mem(frame_pointer+8);
|
||||
|
||||
while (dwarf::at_name(curr_func) != "main") {
|
||||
curr_func = get_func_from_pc(ret_addr);
|
||||
output_frame(curr_func);
|
||||
frame_pointer = read_mem(frame_pointer);
|
||||
return_address = read_mem(frame_pointer+8);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
但是在ARM体系结构中,出于性能的考虑,天才的开发者为了节约R7/R11寄存器,使其可以作为通用寄存器来使用,因此无法保证保存足够的信息来形成上述调用栈的(即使你向编译器传入了“-fno-omit-frame-pointer”)。比如下面两种情况,ARM就会不遵循一般意义上的序言(prologue),感兴趣的同学可以具体查看[APCS Doc](https://www.cl.cam.ac.uk/~fms27/teaching/2001-02/arm-project/02-sort/apcs.txt#1018)。
|
||||
|
||||
<li>
|
||||
函数为叶函数,即在函数体内再没有任何函数调用。
|
||||
</li>
|
||||
<li>
|
||||
函数体非常小。
|
||||
</li>
|
||||
|
||||
## Android中的unwind
|
||||
|
||||
我们知道大部分Android手机使用的都是ARM体系结构,那在Android中需要如何进行unwind呢?我们需要分两种情况分别讨论。
|
||||
|
||||
**1. Debug版本unwind**
|
||||
|
||||
如果是Debug版本,我们可以通过“.debug_frame”(有兴趣的同学可以了解一下[DWARF](http://www.dwarfstd.org/doc/DWARF4.pdf))来帮助我们进行unwind。这种方法十分高效也十分准确,但缺点是调试信息本身很大,甚至会比程序段(.TEXT段)更大,所以我们是无法在Release版本中包含这个信息的。
|
||||
|
||||
>
|
||||
DWARF 是一种标准调试信息格式。DWARF最早与ELF文件格式一起设计, 但DWARF本身是独立的一种对象文件格式。本身DAWRF和ELF的名字并没有任何意义(侏儒、精灵,是不是像魔兽世界的角色 :)),后续为了方便宣传,才命名为Debugging With Attributed Record Formats。引自[wiki](https://en.wikipedia.org/wiki/DWARF#cite_note-eager-1)
|
||||
|
||||
|
||||
**2. Release版本unwind**
|
||||
|
||||
对于Release版本,系统使用了一种类似“.debug_frame”的段落,是更加紧凑的方法,我们可以称之为unwind table,具体来说在x86和ARM64平台上是“.eh_frame”和“.eh_frame_hdr”,在ARM32平台上为“.ARM.extab”和“.ARM.exidx”。
|
||||
|
||||
由于ARM32的标准早于DWARF的方法,所有ARM使用了自己的实现,不过它们的原理十分接近,后续我们只讨论“.eh_frame”,如果你对ARM32的实现特别感兴趣,可以参考[ARM-Unwinding-Tutorial](https://sourceware.org/binutils/docs/as/ARM-Unwinding-Tutorial.html)。
|
||||
|
||||
“.eh_frame section”也是遵循DWARF的格式的,但DWARF本身十分琐碎也十分复杂,这里我们就不深入进去了,只涉及一些比较浅显的内容,你只需要了解DAWRF使用了一个被称为DI(Debugging Information Entry)的数据结构,去表示每个变量、变量类型和函数等在debug程序时需要用到的内容。
|
||||
|
||||
“.eh_frame”使用了一种很聪明的方法构成了一个非常大的表,表中包含了每个程序段的地址对应的相应寄存器的值以及返回地址等相关信息,下面就是这张表的示例(你可以使用`readelf --debug-dump=frames-interp`去查看相应的信息,Release版中会精简一些信息,但所有帮助我们unwind的寄存器信息都会被保留)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/70/03aa1ebfe910222910dee2b23c7a5770.png" alt="">
|
||||
|
||||
“.eh_frame section”至少包含了一个CFI(Call Frame Information)。每个CFI都包含了两个条目:独立的CIE(Common Information Entry)和至少一个FDE(Frame Description Entry)。通常来讲CFI都对应一个对象文件,FDE则描述一个函数。
|
||||
|
||||
“[.eh_frame_hdr](https://refspecs.linuxfoundation.org/LSB_1.3.0/gLSB/gLSB/ehframehdr.html) section”包含了一系列属性,除了一些基础的meta信息,还包含了一列有序信息(初始地址,指向“.eh_frame”中FDE的指针),这些信息按照function排序,从而可以使用二分查找加速搜索。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/d1/ba663ea69e7a64a716e37c7f6710f4d1.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
总的来说,unwind第一个栈帧是最难的,由于ARM无法保证会压基址指针寄存器(EBP)进栈,所以我们需要借助一些额外的信息(.eh_frame)来帮助我们得到相应的基址指针寄存器的值。即使如此,生产环境还是会有各种栈破坏,所以还是有许多工作需要做,比如不同的调试器(GDB、LLDB)或者breakpad都实现了一些搜索算法去寻找潜在的栈帧,这里我们就不展开讨论了,感兴趣的同学可以查阅相关代码。
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
下面给你一些外部链接,你可以阅读GCC中实现unwind的关键函数,有兴趣的同学可以在调试器中实现自己的unwinder。
|
||||
|
||||
<li>
|
||||
[_Unwind_Backtrace](https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git;a=blob;f=libgcc/unwind.inc;h=12f62bca7335f3738fb723f00b1175493ef46345;hb=HEAD#l275)
|
||||
</li>
|
||||
<li>
|
||||
[uw_frame_state_for](https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git;a=blob;f=libgcc/unwind-dw2.c;h=b262fd9f5b92e2d0ea4f0e65152927de0290fcbd;hb=HEAD#l1222)
|
||||
</li>
|
||||
<li>
|
||||
[uw_update_context](https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git;a=blob;f=libgcc/unwind-dw2.c;h=b262fd9f5b92e2d0ea4f0e65152927de0290fcbd;hb=HEAD#l1494)
|
||||
</li>
|
||||
<li>
|
||||
[uw_update_context_1](https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git;a=blob;f=libgcc/unwind-dw2.c;h=b262fd9f5b92e2d0ea4f0e65152927de0290fcbd;hb=HEAD#l1376)
|
||||
</li>
|
||||
|
||||
|
||||
49
极客时间专栏/geek/Android开发高手课/特别放送/专栏学得苦?可能你还需要一份配套学习书单.md
Normal file
49
极客时间专栏/geek/Android开发高手课/特别放送/专栏学得苦?可能你还需要一份配套学习书单.md
Normal file
@@ -0,0 +1,49 @@
|
||||
<audio id="audio" title="专栏学得苦?可能你还需要一份配套学习书单" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/b3/940f244c96efa4e275ec8a427a1f77b3.mp3"></audio>
|
||||
|
||||
你好,我是张绍文。专栏已经发布了一段时间,很多同学在学习专栏时问我,想补充一些基础知识可以参考哪些图书。今天我就结合专栏的编排,给你推荐几本我看过并且对我帮助很大的图书。推荐的书单不在于数量,而在于希望尽可能覆盖Android开发工程师进阶学习的路径,只有掌握牢固的基础知识,才能在进阶的道路上走得平稳。专栏把进阶的各个主题由点到线串联起来,但这背后必然少不了一些基础的、底层的知识进行支撑,而这些经典的图书涵盖的知识点比较全面,即使遇到问题时放在手边也是很好的参考书。
|
||||
|
||||
作为一名Android开发工程师,**你需要学习一些Linux的基础知识,在做优化时可以有更好的思路**。
|
||||
|
||||
关于Linux学习,我推荐:
|
||||
|
||||
[性能之巅](http://book.douban.com/subject/26586598/)
|
||||
|
||||
[最强Android书:架构大剖析](http://book.douban.com/subject/30269276/)
|
||||
|
||||
[戳此购买](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2flgejhhjglnc)
|
||||
|
||||
极客时间专栏:[Linux性能优化实战](http://time.geekbang.org/column/140)
|
||||
|
||||
**如果想更好地学习虚拟机以及Hook相关的知识,你需要对C++以及编译原理有一定的了解。**
|
||||
|
||||
关于虚拟机,我推荐:
|
||||
|
||||
[程序员的自我修养——链接、装载与库](http://book.douban.com/subject/3652388)
|
||||
|
||||
[垃圾回收算法手册](http://book.douban.com/subject/26740958)
|
||||
|
||||
[戳此购买](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2xf8slfumbdtk)
|
||||
|
||||
关于编程语言,我推荐:
|
||||
|
||||
[More Effective C++](http://book.douban.com/subject/5908727/)
|
||||
|
||||
[戳此购买](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2fmqzr7s3bvpk)
|
||||
|
||||
[Effective Java中文版(第3版)](http://book.douban.com/subject/30412517)
|
||||
|
||||
[戳此购买](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F3nffgcb3e14u0)
|
||||
|
||||
**其他的知识,例如网络、数据库的一些细分领域**,我推荐:
|
||||
|
||||
[Web性能权威指南](http://book.douban.com/subject/25856314/)
|
||||
|
||||
[戳此购买](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F3nswzsiy1lro8)
|
||||
|
||||
[UNIX网络编程 卷1:套接字联网API(第3版)](http://book.douban.com/subject/26434583/)
|
||||
|
||||
[戳此购买](time://mall?url=http%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F27cl3htdrxi48)
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。
|
||||
|
||||
|
||||
89
极客时间专栏/geek/Android开发高手课/特别放送/专栏学得苦?可能是方法没找对.md
Normal file
89
极客时间专栏/geek/Android开发高手课/特别放送/专栏学得苦?可能是方法没找对.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<audio id="audio" title="专栏学得苦?可能是方法没找对" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/9d/0dc170b479866380d6f84ccf3d353b9d.mp3"></audio>
|
||||
|
||||
>
|
||||
各位同学,我是《Android开发高手课》的编辑Shawn,很高兴可以和你分享专栏里同学们自己的故事。小说家马塞尔·普鲁斯特说过:“真正的发现之旅,不在于寻找新的风景,而在于有新的视角”。专栏便是给了你一个“高手”的视角,让你重新审视自己工作中处理问题的方法、掌握新的技能,进而让自己也成为真正的高手。这个过程一定是艰辛的,但只要坚持下去,一定能大有收获。
|
||||
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/39/559ec4b6bd32283500cddc2869bad639.png" alt="">
|
||||
|
||||
我是Kenny,来自广州,加入到《Android开发高手课》,是希望自己可以深入学习一些Android的高级知识。
|
||||
|
||||
学习的过程总是枯燥的,我的方法是**保持兴趣和新鲜感。**你可以把专栏的课程当作是一本武功秘籍,每一期就相当于一种招式,这样就会很有期待。我基本会在专栏更新第一时间就学习一遍,然后再结合Google查找跟专栏相关的知识点,把它们融会贯通。另外,专栏里提供的练习我也会先自己思考,然后再尝试去写demo验证,最后跟老师给的Sample进行对照,对比分析我和老师给的方案的优劣。
|
||||
|
||||
如果你正处学习的迷茫期,我建议学习前先带着问题,比如专栏更新后,先想想自己在工作中是否遇到过和这期主题相关的技术问题。然后就是兴趣了,对技术始终保持兴趣,学习的动力也会更充沛,在学习过程中也会产生更多有价值的思考。
|
||||
|
||||
我目前学习最大的收获是**思考问题、处理问题的思维方式的变化**,在对待“黑科技”也会更慎重了。最主要的是思维得到了发散,解决问题时也会多考虑如何举一反三。
|
||||
|
||||
我的2019年“小目标”:目前在从事产品性能优化的工作,期待2019能把产品做成竞品间各项性能指标第一!
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/49/541f8da3cf1bb6340462e2313cc3af49.png" alt="">
|
||||
|
||||
我是ZYW,目前在从事移动端开发,从2008年开始到今天已经10个年头了。
|
||||
|
||||
我主要是在更新当天晚上看专栏的内容,看完后再做练习,遇到不会的内容会去搜索相关的知识点。
|
||||
|
||||
在当下,不论是工作还是学习,焦虑肯定是有的。**坚持,学习从来就不是一蹴而就的事情**。我毕业10年了,IT编码也10年了,现在还在坚持。IT技术更新很快,一定要在合适的时间做合适的事情,不要怕困难,更不要放弃。
|
||||
|
||||
我的2019年“小目标”:好好的工作,好好照顾家人,能在Android的道路一直走下去,坚持。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/96/29cfacdd036206fa1d6f0512b723b096.png" alt="">
|
||||
|
||||
我是Owen,工作3年做过OA和手机的项目,目前在深圳一家上市公司负责海外工具类开发。订阅这个专栏主要是听过江湖上绍文老师的“传说”,再加上我对讲的内容也比较感兴趣。
|
||||
|
||||
一般专栏更新之后,我会选择第一时间听一遍音频,或是在上下班的地铁上,或是在睡觉前。我会利用零碎时间反复听音频,把整片的时间用来反复阅读课程文章,然后根据文章的Sample自己亲自实践。**因为在实践的过程中会有很多坑要爬,需要自己补充老师提到的知识点,专栏的篇幅也有限,有些知识点就需要自己去查找资料来学习**。所以一篇文章我会反复阅读,反复查看相关知识点,反复实践。遇到不懂的就问绍文老师、同学和同事,然后形成自己的理解和总结,争取自己也能写一点总结分享出来。
|
||||
|
||||
坚持学习的过程,我感觉自己拓宽了知识面,对知识点的体会也加深了。移动开发领域大佬、高手如云,必须孜孜不倦地学习和交流,才能做到真正的理解并且达到一定的高度。
|
||||
|
||||
这个专栏整体来说适合爬坡进阶,如果坚持学完会有很多收获。有些知识点我平时没太注意,或者有些知识点平时干脆都没听过,学完给我的感觉我们并不是简单的应用开发,还有太多可以精深的东西。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/9f/bccda15e0955af195cabfcd1baa9789f.png" alt="">
|
||||
|
||||
大家好,我是Seven,来自四川成都。现在在某家医疗公司担任Android开发,主要做的是摄像头开发、实时分析相机预览数据这块,之前还做过一年的视频点播和直播的底层开发。在这两年的工作中,一直想把自己的半吊子基础给打实。
|
||||
|
||||
说来也巧,曾经的项目中用到了Tinker,也就顺理成章进入了Tinker交流群,也在群内交流了一段时间。某一天,绍文老师在群里推荐了他的极客时间专栏;与此同时,众多著名Android大佬和公众号也争相推广,我已经站不住了,技术的浪潮已经向我扑来,我非接不可,同时也希望自己能够得到一次深度成长的机会,所以我选择了订阅,这也是对自己负责吧。
|
||||
|
||||
刚开始时我是很用心的每天都听,然后课后Sample也在认真做,但当我发现Sample搞不定的时候,我就改变了学习态度:专栏更新,我也及时阅读,Sample做不出来就放在那里,反正保持同步阅读专栏,有时也在专栏里留些无关痛痒的话,以为这样就是学习。直到某一天,专栏出了一篇文章[《让Sample跑起来 | 热点问题答疑第1期》](http://time.geekbang.org/column/article/73068),我才恍然大悟,自己当前的学习状态不对,不应该这样学习,应该一丝不苟,脚踏实地才对。**那时的我只是看上去在学习而已,实际根本没有任何收获**。于是我改正了自己的态度,重新打开第1期重新学习,认真阅读并理解文中的每一个字,遇到不懂的地方就看文中给的链接或者自己查资料慢慢摸索。后来发现这样学习虽然慢,但是我心里很踏实,**而且学到的东西其实不止是专栏中的内容,因为在查资料的时候,总会查到别的东西,可以顺带学习,并且做了笔记**。我现在的学习状态大概就是这样,我还是很满意自己做出的改变。
|
||||
|
||||
专栏从开始看到现在这么久了,最主要提升的是我的**自觉性**,看到不懂的知识点我并不会害怕,而是想着要怎么去弄懂它,也不会消极的对待所谓“高深”的知识了。现在遇到一个问题,我脑袋里面想的是要去解决它,先试着自己去解决,解决不了的话说明知识不到位,需要深入学习。
|
||||
|
||||
如果你觉得专栏里的知识很难,也不要消极对待,很多大的知识点都是由许多小知识点组成的,只要分而治之,就可以很快**建立自信**。并且这些小的知识点排列组合会产生无数种知识,所以这里也建议同学们认真对待基础知识,比如专栏里提到的Linux有关的知识,这些都是保质期很长的知识,希望我们可以一起坚持下来。
|
||||
|
||||
我的2019年“小目标”:2018年已经过去,希望2019年的自己,能够对移动开发的基础知识有更深刻的理解,希望自己能够在Android源码分析研究这一块大有长进,一起加油吧!
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/82/1f/826a24c9f4a2bb4d60c553f57fcd491f.png" alt="">
|
||||
|
||||
我是小洁,是一名工作接近4年的Android开发,坐标广东东莞,公司是做儿童电话手表的,我在团队中负责性能相关优化以及组件化优化方面工作。由于目前工作的任务专栏有所涉及,加上身边同事的推荐,所以想从专栏中借鉴学习老师的各种经验。
|
||||
|
||||
由于工作原因,我一般的学习时间主要是在晚上。我会根据自己对当前内容是否感兴趣,以及是否感觉重要而去学习并完成课后的练习。通过专栏我收获了一些老师的经验之谈和对某些方案的思考,能力也有所提升吧。
|
||||
|
||||
我认为学习还是靠**个人毅力**,每当学习感到枯燥或者遇到障碍时,可以沉下来问问自己开始学习的目的,重新调整自己再一步步看下去,终会有豁然开朗的时候。
|
||||
|
||||
我的2019年“小目标”:希望自己能实现一个性能监控分析。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/72/d3b0847e18f8bc4a5d68b4a096f87f72.png" alt="">
|
||||
|
||||
我是希夷,我订阅专栏的目的是想提高自己的眼界和能力。
|
||||
|
||||
我主要是利用空闲的时间学习专栏,并且会做些笔记,但专栏的作业和练习我坚持得不够(捂脸)。**我觉得学习专栏时感觉难是正常的,如果不觉得难就说明这个专栏对你提升有限**。我认为还是贵在坚持,一遍不懂就多看几遍、多练几遍,勤能补拙,我们学习都会有这个过程,也需要我们给自己打打气。
|
||||
|
||||
同样我觉得Android开发,**基础真的很重要**,我感觉自己有很多基础的东西要补。通过这个专栏,我看到了大公司在移动开发这块使用的比较新的技术,确实拓展了自己的视野。
|
||||
|
||||
我的2019年“小目标”:希望把Kotlin和Flutter用到项目中,把从专栏中学到的东西落地到项目实践上,也希望自己有更高的收入。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/7a/2b2f0156a5db6ba1d04505b3dd6b747a.png" alt="">
|
||||
|
||||
我是志伟,从12年开始在芯片企业从事Android ROM开发,15年开始在移动互联网企业做Android开发。我希望可以从专栏里更加全面地了解Android开发的技术,领略绍文面对亿级App各种技术问题的思考方式和处理方法。
|
||||
|
||||
专栏更新当天晚上我都会认真学习,通过文章里附带的外部资料、自己查询网上其他资料和专栏提供的Sample进行练习,从中提炼自己需要的知识。**我还会结合自己的实际工作、以前的经历,推敲一下自己在面对专栏里的问题时会如何思考,我会怎么去做**。
|
||||
|
||||
专栏涉及技术面广,又有深度,花时间深入钻研,肯定有很大的收获。比如我自己,通过学习扩展了自己的知识面,对一些技术的理解也更加深入,很多问题也有了解决方案。专栏定位 “高手”,肯定有觉得难或者不懂的知识点,这就更需要坚持,因为这正是成长的机会。另外,还可以直接在专栏留言区跟作者进行交流,从中我也获益良多。
|
||||
|
||||
绍文经历过多个App日活过亿的成长,期间面临的各种技术难题也是其他App共同存在的,但我们不是每个人都有这样的机会去经历。通过专栏,可以以高度“浓缩”的方式经历一遍,这也是十分宝贵的收获。
|
||||
|
||||
我的2019年“小目标”:朝架构师的方向“一路走到黑”。
|
||||
|
||||
>
|
||||
听完身边同学的故事,你有没有想和其他同学分享的故事呢?欢迎你在留言区也写写自己学习专栏的方法和体会,我们会在下一期答疑文章发布时,选出留言点赞数最高的3位,送出“学习加油礼包”,期待你的分享。
|
||||
|
||||
|
||||
|
||||
89
极客时间专栏/geek/Android开发高手课/特别放送/程序员修炼之路 | 设计能力的提升途径.md
Normal file
89
极客时间专栏/geek/Android开发高手课/特别放送/程序员修炼之路 | 设计能力的提升途径.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<audio id="audio" title="程序员修炼之路 | 设计能力的提升途径" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/71/19ab12c6b85b340100840a89a22c2671.mp3"></audio>
|
||||
|
||||
>
|
||||
你好,我是张绍文,今天我要和你分享我的朋友长元的一篇文章,主题是设计能力的提升途径。专栏已经进入架构演进模块,由于每个人对架构的理解都不同,在工作中也会遇到各种各样的架构设计问题,很多时候我们的架构设计能力都是靠不断的理论学习和在设计实践中不断摸索提高的,因此在成为设计高手的道路上,我们肯定或多或少有些自己的经验和体会,当然也少不了踩坑。今天长元分享的设计能力提升路径,希望可以把他的经验分享给你,你可以参考他的提升路径来强化自己的设计能力,在高手的修炼之路上少走弯路。
|
||||
|
||||
|
||||
每当我做完一次内部设计培训以后,经常有同学来问我:如何才能快速提升自己的设计能力?我觉得这个问题非常有代表性,代表了一大波程序员在艰辛修炼路上的心声。今天我就来分享一下我所理解的程序员设计能力的提升路径,也欢迎你留言写写你的思考与体会。
|
||||
|
||||
**1. 编码历练**
|
||||
|
||||
代码行经验是个非常重要的东西,当你还没有1万行代码经验的时候,如果你来问我如何提升设计能力这个问题,我只能告诉你不要太纠结,看看理论就好,老老实实先写代码吧。
|
||||
|
||||
一个程序员平均每天码代码的速度是200~300行。你可能会说,我一天怎么也要写上1000行吧?别忘了,当你码完代码后,你还需要测试、调试、优化、Bug Fix,这些时间你没法一直码代码的。
|
||||
|
||||
编码规范就不多说了,如果你的代码还是杂乱无章的状态,就先别谈什么设计与架构了,先把基础的工作做好再谈其他的吧。
|
||||
|
||||
另外,作为“代码洁癖患者”,推荐你不要在写完代码后,再做批量格式化处理,或者手工再去整理代码,而是应该每敲一个字符,都是符合规范的。习惯真的很重要,有时在招聘面试的时候,我真想添加一个环节,现场编写程序完成一个简单但容易出错的任务,考察一下你的代码基本功。
|
||||
|
||||
**2. 理论学习**
|
||||
|
||||
简单说就是看书、看博客,学习你能得到的所有资源,但前提是内容质量要高。例如图书,我推荐:《重构 改善既有代码的设计》《敏捷软件开发:原则、模式与实践》《UML和模式应用》《设计模式》等,其他你还需要学习面向对象设计原则(五大原则)。
|
||||
|
||||
《设计模式》是本很古老的书了,只有短短200页,但是可能这是最难看懂的一本书了,可能一个月都看不完(看小说的话,200页3个小时也许就看完了吧)。而且就算看完了,也不会全看懂,很可能看懂的内容不超过30%。我想说的是,看不懂没关系,认真看了就行,不用太纠结,因为这不能说明什么问题。
|
||||
|
||||
另外,我想说一下,多线程技术是程序员必须掌握的,而且需要理解透彻。现在的高级技术例如GCD,会掩盖你对多线程理解不足的问题,因为使用起来实在太简单了。另外,别说你没写过多线程依然完成了复杂的项目,更别说你随手写出的多线程代码好像也没出什么问题啊,你可以试试把你的代码给技术好的同事看看,分分钟写个Demo让它出错乃至崩溃。
|
||||
|
||||
**3. 实践**
|
||||
|
||||
现在,你已经具备了一定的编码经验,而且已经学习了足够的理论知识,接下来就是真正练手的时候了。好好反复思考你学习的这些理论知识,要如何运用到项目中去,通过身体力行的实践,一定要把那些理论搞清楚,用于指导你的实践。在实践的过程中,你要收起从前的自信,首先否定自己以前的做法,保证每次做出的东西相比以前是有进步、有改进的。
|
||||
|
||||
**4. 重温理论**
|
||||
|
||||
你已经能看到自己的进步了,发现比以前做得更好了,但是总感觉还不够,好像有瓶颈似的,恭喜你,已经可以看到你未来的潜力了。
|
||||
|
||||
重新拿起书本,重温一遍之前看的那些似懂非懂的东西,你会发现之前没弄懂的内容,现在豁然开朗了,不再有那种难于理解的晦涩感了。而且就算是以前你觉得自己已经理解的内容,再看一遍的话,通常也会有新的收获。
|
||||
|
||||
**5. 再实践**
|
||||
|
||||
这个阶段,你已经掌握了较多的知识,不但实践经验丰富,各种理论也能手到擒来了。但是,你发现你的设计依然不够专业,而且回过头去看以前写的代码,你会惊讶:天啊,这是谁写的代码,怎么能这样干!然后,就不多说了…此时,你已经进入了自省的阶段,掌握了适合自己的学习方法,之后再学习什么新东西,都不会再难住你了。
|
||||
|
||||
**6. 总结**
|
||||
|
||||
先别太得意(不信?那你去给团队分享一次讲座试试),你还需要总结,总结自己的学习方法、总结项目经验、总结设计理论的知识。
|
||||
|
||||
如果你能有自己独到的理解,而不是停留在只会使用成熟的设计模式什么的,能根据自己的经验教训总结出一些设计原则,那自然是极好的。
|
||||
|
||||
**7. 分享**
|
||||
|
||||
分享是最好的学习催化剂,当你要准备一次培训分享的时候,你会发现先前以为已经理解的东西其实并没有完全理解透彻,因为你无法把它讲清楚,实际上还是研究得不够透彻。这时会迫使你再重新深入学习,做到融汇贯通,然后你才敢走上讲台。否则,当别人提问的时候,你根本回答不上来。
|
||||
|
||||
以上,便是我认为的程序员修炼道路的必经阶段。接下来,我再分享几点其他对设计能力提升非常重要的方法。
|
||||
|
||||
- **养成先设计,再编码的习惯。**
|
||||
|
||||
几乎所有的程序员,一开始都不太愿意写文档,也不太愿意去精心设计,拿到需求总是忍不住那双躁动的手,总觉得敲在键盘上,把一行一行的代码飙出来,才有成就感,才是正确的工作姿势。
|
||||
|
||||
我的建议是,没讨论清楚不要编码,不然你一定会返工。
|
||||
|
||||
- **设计重于编码,接口重于实现。**
|
||||
|
||||
制定接口的过程,本身就是设计过程,接口一定要反复推敲,尽量做减法而不是加法,在能满足需求的情况下越简单越好。
|
||||
|
||||
另外,不要一个人冥思苦想。可以先简单做一个雏形出来,然后去找使用方沟通,直到对方满意为止。不要完全根据使用需求去设计接口,参考MVVM,ViewModel就是根据View的需要而对Model进行的再封装,不能将这些接口直接设计到Model中。
|
||||
|
||||
- **不盲从设计模式。**
|
||||
|
||||
设计模式只是一种解决问题的套路方法,你也可以有自己的方法,当然设计模式如果用好了,会让你的设计显得专业、优雅,毕竟前辈们的心血结晶是非常有价值的。但是如果滥用的话,也会导致更严重的问题,甚至可能成为灾难。我觉得面向对象设计原则更加重要,有些原则是必须遵守的(如单向依赖、SRP等),而设计模式本身都是遵守这些原则的,有些模式就是为了遵循某原则而设计出来的。
|
||||
|
||||
抽象不是万能的,在适当的地方使用,需要仔细推敲。当有更好的方案不用抽象就能解决问题时,尽量避免抽象。我见过太多抽象过火、过度设计的案例了,增加了太多维护成本,还不如按照最自然的方式去写。
|
||||
|
||||
- **空杯心态,向身边的同学学习,站在巨人的肩上,站在别人的肩上。**
|
||||
|
||||
有人提意见,先收下它(无论接受与否)。
|
||||
|
||||
很多程序员都有个“毛病”,就是觉得自己技术牛的不行,不愿意接受别人的意见,尤其是否定意见(文人相轻)。 但是无论是理论的学习,还是编码实践,向身边的同学学习是对自己影响最大的(三人行,必有我师)。
|
||||
|
||||
我自己就经常在跟团队同学讨论中获益,当百思不得其解的时候,把问题抛出来讨论一下,通常都能得到一个最佳方案。
|
||||
|
||||
另外,跟团队其他人讨论还有一个好处,就是当你的设计有妥协或有些不专业的时候,别人看到代码也不会产生质疑,因为他也参与了讨论,你不用花那么多时间去做解释。
|
||||
|
||||
设计期间一定要找其他人一起讨论,我一直比较反对一个人把设计做完、把文档写完,然后才找大家开个评审会那种模式,虽然也有效果,但是效果达不到极致。因为大家没有参与到设计中,通过一次会议的时间理解不一定有那么深,最关键的是,如果在会上发现设计有些问题,但不是致命问题的时候,通常并不会打回重新设计。
|
||||
|
||||
相反,如果前期讨论足够,大家都知道你的思路与方案,而且最后也有设计文档,当其他人阅读你的代码的时候,根本无需你再指引,这样今后在工作交接时都会很顺利,何乐而不为呢?
|
||||
|
||||
最后,我想呼吁一下,当你去修改维护别人的代码时,最好找模块负责人深入讨论沟通一下,让他明白你的需求以及你的方案,请他帮忙评估方案是否可行,是否会踩坑、埋坑等。如果你恰好是模块的负责人,请行使你的权力,拒绝有问题的不符合要求的代码提交入库。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。
|
||||
|
||||
|
||||
174
极客时间专栏/geek/Android开发高手课/特别放送/聊聊Framework的学习方法.md
Normal file
174
极客时间专栏/geek/Android开发高手课/特别放送/聊聊Framework的学习方法.md
Normal file
@@ -0,0 +1,174 @@
|
||||
<audio id="audio" title="聊聊Framework的学习方法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/43/3b/434c51aa49ed6f1d4a842dc082595d3b.mp3"></audio>
|
||||
|
||||
大家好,我是陆晓明,现在在一家互联网手机公司担任Android系统开发工程师。很高兴可以在极客时间Android开发高手课专栏里,分享一些我在手机行业9年的经验以及学习Android的方法。
|
||||
|
||||
今天我要跟你分享的是Framework的学习和调试的方法。
|
||||
|
||||
首先,Android是一种基于Linux的开放源代码软件栈,为广泛的设备和机型而创建。下图是Android平台的[主要组件](https://developer.android.google.cn/guide/platform)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/df/90763fd9662c8a75553dc92a78112ddf.png" alt="">
|
||||
|
||||
从图中你可以看到主要有以下几部分组成:
|
||||
|
||||
- **Linux内核**
|
||||
- **Android Runtime**
|
||||
- **原生C/C++库**
|
||||
- Java API框架(后面我称之为Framework框架层)
|
||||
- **系统应用**
|
||||
|
||||
我们在各个应用市场看到的,大多是第三方应用,也就是安装在data区域的应用,它们可以卸载,并且权限也受到一些限制,比如不能直接设置时间日期,需要调用到系统应用设置里面再进行操作。
|
||||
|
||||
而我们在应用开发过程中使用的四大组件,便是在Framework框架层进行实现,应用通过约定俗成的规则,在AndroidMainfest.xml中进行配置,然后继承对应的基类进行复写。系统在启动过程中解析AndroidMainfest.xml,将应用的信息存储下来,随后根据用户的操作,或者系统的广播触发,启动对应的应用。
|
||||
|
||||
那么,我们先来看看Framework框架层都有哪些东西。
|
||||
|
||||
Framework框架层是应用开发过程中,调用的系统方法的内部实现,比如我们使用的TextView、Button控件,都是在这里实现的。再举几个例子,我们调用ActivityManager的getRunningAppProcesses方法查看当前运行的进程列表,还有我们使用NotificationManager的notify发送一个系统通知。
|
||||
|
||||
让我们来看看Framework相关的代码路径。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/17/d4/178ef00a181a85e85a3b75d4c60abcd4.jpg" alt="">
|
||||
|
||||
如何快速地学习、梳理Framework知识体系呢?常见的学习方法有下面几种:
|
||||
|
||||
- 阅读书籍(方便梳理知识体系,但对于解决问题只能提供方向)。
|
||||
- 直接阅读源码(效率低,挑战难度大)。
|
||||
- 打Log和打堆栈 (效率有所提升,但需要反复编译,添加Log和堆栈代码)。
|
||||
- 直接联调,实时便捷(需要调试版本)。
|
||||
|
||||
首先可以通过购买相关的书籍进行学习,其中主要的知识体系有Linux操作系统,比如进程、线程、进程间通信、虚拟内存,建立起自己的软件架构。在此基础上学习Android的启动过程、服务进程SystemServer的创建、各个服务线程(AMS/PMS等)的创建过程,以及Launcher的启动过程。熟悉了这些之后,你还要了解ART虚拟机的主要工作原理,以及init和Zygote的主要工作原理。之后随着在工作和实践过程中你会发现,Framework主要是围绕应用启动、显示、广播消息、按键传递、添加服务等开展,这些代码的实现主要使用的是Java和C++这两种语言。
|
||||
|
||||
通过书籍或者网络资料学习一段时间后,你会发现很多问题都没有现成的解决方案,而此时就需要我们深入源码中进行挖掘和学习。但是除了阅读官方文档外,别忘了调试Framework也是一把利刃,可以让你游刃有余快速定位和分析源码。
|
||||
|
||||
下面我们来看看调试Framework的Java部分,关于C++的部分,需要使用GDB进行调试,你可以在课下实践一下,调试的过程可以参考[《深入Android源码系列(一)》](https://mp.weixin.qq.com/s/VSVUbaEIfrmFZMB1k49fyA)。
|
||||
|
||||
我们这里使用Android Studio进行调试,在调试前我们要先掌握一些知识。Java代码的调试,主要依据两个因素,一个是你要调试的进程;一个是调试的类对应的包名路径,同时还要保证你所运行的手机环境和你要调试的代码是匹配的。只要这两个信息匹配,编译不通过也是可以进行调试的。
|
||||
|
||||
我们调试的系统服务是在SystemServer进程中,可以使用下面的命令验证(我这里使用Genymotion上安装Android对应版本镜像的环境演示)。
|
||||
|
||||
```
|
||||
ps -A |grep system_server 查看系统服务进程pid
|
||||
cat /proc/pid/maps |grep services 通过cat查看此进程的内存映射,看看是否services映射到内存里面。
|
||||
|
||||
```
|
||||
|
||||
这里我们看到信息:/system/framework/oat/x86/services.odex 。
|
||||
|
||||
odex是Android系统对于dex的进一步优化,目的是为了提升执行效率。从这个信息便可以确定,我们的services.jar确实是跑到这里了,也就是我们的系统服务相关联的代码,可以通过调试SystemServer进程进行跟踪。
|
||||
|
||||
下来我们来建立调试环境。
|
||||
|
||||
<li>
|
||||
打开Genymotion,选择下载好Android 9.0的镜像文件,启动模拟器。
|
||||
</li>
|
||||
<li>
|
||||
找到模拟器对应的ActivityManagerService.java代码。 我是从[http://androidxref.com/](http://androidxref.com/)下载Android 9.0对应的代码。
|
||||
</li>
|
||||
<li>
|
||||
打开Android Studio,File -> New -> New Project然后直接Next直到完成就行。
|
||||
</li>
|
||||
<li>
|
||||
新建一个包名,从ActivityManagerService.java文件中找到它,这里为`com.android.server.am`,然后把ActivityManagerService.java放到里面即可。
|
||||
</li>
|
||||
<li>
|
||||
在ActivityManagerService.java的startActivity方法上面设置断点,然后找到菜单的Run -> Attach debugger to Android process勾选Show all process,选中SystemServer进程确定。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/f0/ba1eb6bded9167f26ae48b34a6d792f0.png" alt="">
|
||||
|
||||
这时候我们点击Genymotion模拟器中桌面的一个图标,启动新的界面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/45/c92b62d1065f967696dbdd2851037b45.png" alt="">
|
||||
|
||||
会发现这时候我们设定的断点已经生效。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/05/763f222e01a30c969024d8cf77dd0705.png" alt="">
|
||||
|
||||
你可以看到断下来的堆栈信息,以及一些变量值,然后我们可以一步步调试下去,跟踪启动的流程。
|
||||
|
||||
对于学习系统服务线程来讲,通过调试可以快速掌握流程,再结合阅读源码,便可以快速学习,掌握系统框架的整个逻辑,从而节省学习的时间成本。
|
||||
|
||||
以上我们验证了系统服务AMS服务代码的调试,其他服务调试方法也是一样,具体的线程信息,可以使用下面的命令查看。
|
||||
|
||||
```
|
||||
ps -T 353
|
||||
这里353是使用ps -A |grep SystemServer查出 SystemServer的进程号
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/a8/62d0d79e490a14f19422486c5da85fa8.png" alt="">
|
||||
|
||||
在上面图中,PID = TID的只有第一行这一行,如果PID = TID的话,也就是这个线程是主线程。下面是我们平时使用Logcat查看输出的信息。
|
||||
|
||||
```
|
||||
03-10 09:33:01.804 240 240 I hostapd : type=1400 audit(0.0:1123): avc: de
|
||||
03-10 09:33:37.320 353 1213 D WificondControl: Scan result ready event
|
||||
03-10 09:34:00.045 404 491 D hwcomposer: hw_composer sent 6 syncs in 60s
|
||||
|
||||
```
|
||||
|
||||
这里我还框了一个ActivityManager的线程,这个是线程的名称,通过查看这行的TID(368)就知道下面的Log就是这个线程输出的。
|
||||
|
||||
```
|
||||
03-10 08:47:33.574 353 368 I ActivityManager: Force stopping com.android.providers
|
||||
|
||||
```
|
||||
|
||||
学习完上面的知识,相信你应该学会了系统服务的调试。通过调试分析,我们便可以将系统服务框架进行庖丁解牛般的学习,面对大量庞杂的代码掌握起来也可以轻松一些。
|
||||
|
||||
我们回过头来,再次在终端中输入`ps -A`,看看下面这一段信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/4e/298cadbc90a1f04d02e1e116f6db464e.png" alt="">
|
||||
|
||||
你可以看到这里的第一列,代表的是当前的用户,这里有system root和u0_axx,不同的用户有不同的权限。我们当前关注的是第二列和第三列,第二列代表的是PID,也就是进程ID;第三列代表的是PPID,也就是父进程ID。
|
||||
|
||||
你发现我这里框住的都是同一个父进程,那么我们来找下这个323进程,看看它到底是谁。
|
||||
|
||||
```
|
||||
root 323 1 1089040 127540 poll_schedule_timeout f16fcbc9 S zygote
|
||||
|
||||
```
|
||||
|
||||
这个名字在学习Android系统的时候,总被反复提及,因为它是我们Android世界的孵化器,每一个上层应用的创建,都是通过Zygote调用fork创建的子进程,而子进程可以快速继承父进程已经加载的资源库,这里主要指的是应用所需的JAR包,比如/system/framework/framework.jar,因为我们应用所需的基础控件都在这里,像View、TextView、ImageView。
|
||||
|
||||
接下来我来讲解下一个调试,也就是对TextView的调试(其他Button调试方式一样)。如前面所说,这个代码被编译到/system/framework/framework.jar,那么我们通过ps命令和cat /proc/pid/maps命令在Zygote中找到它,同时它能够被每一个由Zygote创建的子进程找到,比如我们当前要调试Gallery的主界面TextView。
|
||||
|
||||
我们验证下,使用`ps -A |grep gallery3d`查到Gallery对应的进程PID,使用cat /proc/pid/maps |grep framework.jar看到如下信息:
|
||||
|
||||
```
|
||||
efcd5000-efcd6000 r--s 00000000 08:06 684 /system/framework/framework.jar
|
||||
|
||||
```
|
||||
|
||||
这说明我们要调试的应用进程在内存映射中确实存在,那么我们就需要在gallery3d进程中下断点了。
|
||||
|
||||
下来我们建立调试环境:
|
||||
|
||||
<li>
|
||||
打开Genymotion,选择下载好Android 9.0的镜像文件,启动模拟器,然后在桌面上启动Gallery图库应用。
|
||||
</li>
|
||||
<li>
|
||||
找到模拟器对应的TextView.java代码。
|
||||
</li>
|
||||
<li>
|
||||
打开Android Studio,File -> New -> New Project然后直接Next直到完成就行。
|
||||
</li>
|
||||
<li>
|
||||
新建一个包名,从TextView.java文件中找到它的包名,这里为android.widget,然后把TextView.java放到里面即可。
|
||||
</li>
|
||||
<li>
|
||||
在TextView.java的onDraw方法上面设置断点,然后找到菜单的Run -> Attach debugger to Android process勾选Show all process,选中com.android.gallery3d进程(我们已知这个主界面有TextView控件)确定。
|
||||
</li>
|
||||
|
||||
然后我们点击下这个界面左上角的菜单,随便选择一个点击,发现断点已生效,具体如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/85/c2a9a5a71d4bd4a02b5bee113d866b85.png" alt="">
|
||||
|
||||
然后我们可以使用界面上的调试按钮(或者快捷键)进行调试代码。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/f8/c395c9f16a7c057c1076b4619dd1b5f8.png" alt="">
|
||||
|
||||
今天我讲解了如何调试Framework中的系统服务进程的AMS服务线程,其他PMS、WMS的调试方法跟AMS一样。并且我也讲解了如何调试一个应用里面的TextView控件,其他的比如Button、ImageView调试方法跟TextView也是一样的。
|
||||
|
||||
通过今天的学习,我希望能够给你一个学习系统框架最便捷的路径。在解决系统问题的时候,你可以方便的使用调试分析,从而快速定位、修复问题。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user