This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
<audio id="audio" title="练习Sample跑起来 | ASM插桩强化练习" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1e/c1/1e9062439ee220c56a4420aaf498f7c1.mp3"></audio>
你好,我是孙鹏飞。
专栏上一期绍文讲了编译插桩的三种方法AspectJ、ASM、ReDex以及它们的应用场景。学完以后你是不是有些动心想赶快把它们应用到实际工作中去。但我也还了解到不少同学其实接触插桩并不多在工作中更是很少使用。由于这项技术太重要了可以实现很多功能所以我还是希望你通过理论 + 实践的方式尽可能掌握它。因此今天我给你安排了一期“强化训练”,希望你可以趁热打铁,保持学习的连贯性,把上一期的理论知识,应用到今天插桩的练习上。
为了尽量降低上手的难度,我尽量给出详细的操作步骤,相信你只要照着做,并结合专栏上期内容的学习,你一定可以掌握插桩的精髓。
## ASM插桩强化练习
<img src="https://static001.geekbang.org/resource/image/e2/07/e2f777c2fb2ed535be7367643e43c307.png" alt="">
在上一期里Eateeer同学留言说得非常好提到了一个工具我也在使用这个工具帮助自己理解ASM。安装“ASM Bytecode Outline”也非常简单只需要在Android Studio中的Plugin搜索即可。
<img src="https://static001.geekbang.org/resource/image/7a/47/7ad456d5f6d5054d6259f66a41cb6047.png" alt="">
ASM Bytecode Outline插件可以快速展示当前编辑类的字节码表示也可以展示出生成这个类的ASM代码你可以在Android Studio源码编译框内右键选择“Show Bytecode Outline“来查看反编译后的字节码在右侧展示。
我以今天强化练习中的[SampleApplication](https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM/blob/master/ASMSample/src/main/java/com/sample/asm/SampleApplication.java)类为例,具体字节码如下图所示。
<img src="https://static001.geekbang.org/resource/image/fd/bc/fd7c472e83d37fa3a55124309bcb10bc.png" alt="">
除了字节码模式ASM Bytecode Outline还有一种“ASMified”模式你可以看到SampleApplication类应该如何用ASM代码构建。
<img src="https://static001.geekbang.org/resource/image/f7/66/f7f75f73002335d89289bf03636a6f66.png" alt="">
下面我们通过两个例子的练习加深对ASM使用的理解。
**1. 通过ASM插桩统计方法耗时**
今天我们的第一个练习是通过ASM实现统计每个方法的耗时。怎么做呢请你先不要着急同样以SampleApplication类为例如下图所示你可以先手动写一下希望实现插桩前后的对比代码。
<img src="https://static001.geekbang.org/resource/image/f2/dd/f2bf3b43308b42b78a865f7b36209ddd.png" alt="">
那这样“差异”代码怎么样转化了ASM代码呢ASM Bytecode Outline还有一个非常强大的功能它可以展示相邻两次修改的代码差异这样我们可以很清晰地看出修改的代码在字节码上的呈现。
<img src="https://static001.geekbang.org/resource/image/b6/e5/b6502906622a46a638dd9f3af10619e5.png" alt="">
“onCreate”方法在“ASMified”模式的前后差异代码也就是我们需要添加的ASM代码。在真正动手去实现插桩之前我们还是需要理解一下ASM源码中关于Core API里面ClassReader、ClassWriter、ClassVisitor等几个类的用法。
我们使用ASM需要先通过ClassReader读入Class文件的原始字节码然后使用ClassWriter类基于不同的Visitor类进行修改其中COMPUTE_MAXS和EXPAND_FRAMES都是需要特别注意的参数。
```
ClassReader classReader = new ClassReader(is);
//COMPUTE_MAXS 说明使用ASM自动计算本地变量表最大值和操作数栈的最大值
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
//EXPAND_FRAMES 说明在读取 class 的时候同时展开栈映射帧(StackMap Frame),在使用 AdviceAdapter里这项是必须打开的
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
```
如果要统计每个方法的耗时我们可以使用AdviceAdapter来实现。它提供了onMethodEnter()和onMethodExit()函数,非常适合实现方法的前后插桩。具体的实现,你可以参考今天强化练习中的[TraceClassAdapter](https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM/blob/master/asm-gradle-plugin/src/main/java/com/geektime/asm/ASMCode.java#L60)的实现:
```
private int timeLocalIndex = 0;
@Override
protected void onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, &quot;java/lang/System&quot;, &quot;currentTimeMillis&quot;, &quot;()J&quot;, false);
timeLocalIndex = newLocal(Type.LONG_TYPE); //这个是LocalVariablesSorter 提供的功能,可以尽量复用以前的局部变量
mv.visitVarInsn(LSTORE, timeLocalIndex);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitMethodInsn(INVOKESTATIC, &quot;java/lang/System&quot;, &quot;currentTimeMillis&quot;, &quot;()J&quot;, false);
mv.visitVarInsn(LLOAD, timeLocalIndex);
mv.visitInsn(LSUB);//此处的值在栈顶
mv.visitVarInsn(LSTORE, timeLocalIndex);//因为后面要用到这个值所以先将其保存到本地变量表中
int stringBuilderIndex = newLocal(Type.getType(&quot;java/lang/StringBuilder&quot;));
mv.visitTypeInsn(Opcodes.NEW, &quot;java/lang/StringBuilder&quot;);
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, &quot;java/lang/StringBuilder&quot;, &quot;&lt;init&gt;&quot;, &quot;()V&quot;, false);
mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);//需要将栈顶的 stringbuilder 保存起来否则后面找不到了
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitLdcInsn(className + &quot;.&quot; + methodName + &quot; time:&quot;);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, &quot;java/lang/StringBuilder&quot;, &quot;append&quot;, &quot;(Ljava/lang/String;)Ljava/lang/StringBuilder;&quot;, false);
mv.visitInsn(Opcodes.POP);//将 append 方法的返回值从栈里 pop 出去
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, &quot;java/lang/StringBuilder&quot;, &quot;append&quot;, &quot;(J)Ljava/lang/StringBuilder;&quot;, false);
mv.visitInsn(Opcodes.POP);//将 append 方法的返回值从栈里 pop 出去
mv.visitLdcInsn(&quot;Geek&quot;);
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, &quot;java/lang/StringBuilder&quot;, &quot;toString&quot;, &quot;()Ljava/lang/String;&quot;, false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, &quot;android/util/Log&quot;, &quot;d&quot;, &quot;(Ljava/lang/String;Ljava/lang/String;)I&quot;, false);//注意: Log.d 方法是有返回值的,需要 pop 出去
mv.visitInsn(Opcodes.POP);//插入字节码后要保证栈的清洁,不影响原来的逻辑,否则就会产生异常,也会对其他框架处理字节码造成影响
}
```
具体实现和我们在ASM Bytecode Outline看到的大同小异但是这里需要注意局部变量的使用。在练习的例子中用到了AdviceAdapter的一个很重要的父类LocalVariablesSorter这个类提供了一个很好用的方法newLocal它可以分配一个本地变量的index而不用用户考虑本地变量的分配和覆盖问题。
另一个需要注意的情况是我们在最后的时候需要判断一下插入的代码是否会在栈顶上遗留不使用的数据如果有的话需要消耗掉或者POP出去否则就会导致后续代码的异常。
这样我们就可以快速地将这一大段字节码完成了。
**2. 替换项目中的所有的new Thread**
今天另一个练习是替换项目中所有的new Thread换为自己项目的CustomThread类。在实践中你可以通过这个方法在CustomThread增加统计代码从而实现统计每个线程运行的耗时。
不过这也是一个相对来说坑比较多的情况,你可以提前考虑一下可能会遇到什么状况。同样我们通过修改[MainActivity](https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM/blob/master/ASMSample/src/main/java/com/sample/asm/MainActivity.java#L20)的startThread方法里面的Thread对象改变成CustomThread通过ASM Bytecode Outline看看在字节码上面的差异
<img src="https://static001.geekbang.org/resource/image/a7/0a/a7579f0e2e6fc1df1fa7b880946c740a.png" alt="">
InvokeVirtual是根据new出来的对象来调用所以我们只需要替换new对象的过程就可以了。这里需要处理两个指令一个new、一个InvokeSpecial。在大多数情况下这两条指令是成对出现的但是在一些特殊情况下会遇到直接从其他位置传递过来一个已经存在的对象并强制调用构造方法的情况。
而我们需要处理这种特殊情况所以在例子里我们需要判断new和InvokeSpecial是否是成对出现的。
```
private boolean findNew = false;//标识是否遇到了new指令
@Override
public void visitTypeInsn(int opcode, String s) {
if (opcode == Opcodes.NEW &amp;&amp; &quot;java/lang/Thread&quot;.equals(s)) {
findNew = true;//遇到new指令
mv.visitTypeInsn(Opcodes.NEW, &quot;com/sample/asm/CustomThread&quot;);//替换new指令的类名
return;
}
super.visitTypeInsn(opcode, s);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
//需要排查CustomThread自己
if (&quot;java/lang/Thread&quot;.equals(owner) &amp;&amp; !className.equals(&quot;com/sample/asm/CustomThread&quot;) &amp;&amp; opcode == Opcodes.INVOKESPECIAL &amp;&amp; findNew) {
findNew= false;
mv.visitMethodInsn(opcode, &quot;com/sample/asm/CustomThread&quot;, name, desc, itf);//替换INVOKESPECIAL 的类名,其他参数和原来保持一致
return;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
```
new指令的形态相对特殊比如我们可能会遇到下面的情况
```
new A(new B(2));
```
字节码如下你会发现两个new指令连在一起。
```
NEW A
DUP
NEW B
DUP
ICONST_2
INVOKESPECIAL B.&lt;init&gt; (I)V
INVOKESPECIAL A.&lt;init&gt; (LB;)V
```
虽然ASM Bytecode Outline工具可以帮助我们完成很多场景下的ASM需求但是在处理字节码的时候还是需要考虑很多种可能出现的情况这点需要你注意一下每个指令的特征。所以说在稍微复杂一些的情况下我们依然需要对ASM字节码以及ASM源码中的一些工具类有所了解并且需要很多次的实践毕竟实践是最重要的。
最后再留给你一个思考题如何给某个方法增加一个try catch呢你可以尝试一下在今天强化练习的代码里根据我提供的插件示例实现一下。
强化练习的代码:[https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM](https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM)
## 福利彩蛋
学到这里相信你肯定会认同成为一个Android开发高手的确不容易能够坚持学习和练习并整理输出分享更是不易。但是也确实有同学坚持下来了。
还记得在专栏导读里我们的承诺吗我们会选出坚持参与学习并分享心得的同学送出2019年GMTC大会的门票。今天我们就来兑现承诺送出价值4800元的GMTC门票一张。获得这个“大礼包”的同学是@唯鹿他不仅提交了作业更是在博客里分享了每个练习Sample实现的过程和心得并且一直在坚持。我在文稿里贴了他的练习心得文章链接如果你对于之前的练习Sample还有不明白的地方可以参考唯鹿同学的实现过程。
<li>
[Android 开发高手课 课后练习1 ~ 5](https://blog.csdn.net/qq_17766199/article/details/85716750)
</li>
<li>
[Android 开发高手课 课后练习6 ~ 8121719](https://blog.csdn.net/qq_17766199/article/details/86770948)
</li>
<li>
[专栏第4期完成作业](https://github.com/simplezhli/Chapter04)
</li>
<li>
[专栏第19期完成作业](https://github.com/simplezhli/Chapter19)
</li>
GMTC门票还有剩余给自己一个进阶的机会从现在开始一切都还来得及。
>
<p>小程序、Flutter、移动AI、工程化、性能优化…大前端的下一站在哪里GMTC 2019全球大前端技术大会将于6月北京盛大开幕来自Google、BAT、美团、京东、滴滴等一线前端大牛将与你面对面共话前端那些事聊聊大前端的最新技术趋势和最佳实践案例。<br>
目前大会最低价7折购票火热进行中讲师和议题也持续招募中点击下方图片了解更多大会详情</p>
[<img src="https://static001.geekbang.org/resource/image/e6/68/e65943bb1d18357a19b7121678b78b68.png" alt="">](http://gmtc2019.geekbang.org/?utm_source=wechat&amp;utm_medium=geektime&amp;utm_campaign=yuedu&amp;utm_term=0223)

View File

@@ -0,0 +1,416 @@
<audio id="audio" title="练习Sample跑起来 | 唯鹿同学的练习手记 第1辑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f3/84/f3d7942f70a8ecc897e986d93a5f8584.mp3"></audio>
>
你好我是张绍文今天我要跟你分享唯鹿同学完成专栏课后练习作业的“手记”。专栏承诺会为坚持完成练习作业的同学送出GMTC大会门票唯鹿同学通过自己的努力和坚持为自己赢得了GMTC大会的门票。
如果你还没开始练习我强烈建议你花一些时间在练习上因为每个练习的Sample都是我和学习委员花费很多精力精心准备的为的是让你在学习完后可以有机会上手实践帮你尽快消化专栏里的知识并为自己所用。
大家好我是唯鹿来自西安从事Android开发也有近5年的时间了目前在做智慧社区方面的业务。我自己坚持写博客已经有三年多的时间了希望分享自己在工作、学习中的收获。
先说说我学习专栏的方法专栏更新当天我就会去学习但是难度真的不小。我对自己的要求并不是看一遍就要搞明白而是遇见不懂的地方立马查阅资料要做到大体了解整篇内容。之后在周末的时候我会集中去做Sample练习一边复习本周发布的内容一边用写博客的方式记录练习的结果。
后面我计划专栏结束后再多看、多练习几遍不断查漏补缺。说真的我很喜欢《Android开发高手课》的难度让我在完成练习作业时有种翻越高山的快感。最后希望同学们一起坚持享受翻越高山带来的成就感。
最近在学习张绍文老师的《Android开发高手课》。课后作业可不是一般的难最近几天抽空练习了一下结合老师给的步骤和其他同学的经验完成了前5课的内容。
我整理总结了一下,分享出来,希望可以帮到一起学习的同学(当然希望大家尽量靠自己解决问题)。
[**Chapter01**](https://github.com/AndroidAdvanceWithGeektime/Chapter01)
>
例子里集成了Breakpad来获取发生Native Crash时候的系统信息和线程堆栈信息。通过一个简单的Native崩溃捕获过程完成minidump文件的生成和解析在实践中加深对Breakpad工作机制的认识。
直接运行项目按照README.md的步骤操作就行。
中间有个问题老师提供的minidump_stackwalker工具在macOS 10.14以上无法成功执行因为没有libstdc++.6.dylib库所以我就下载Breakpad源码重新编译了一遍。
使用minidump_stackwalker工具来根据minidump文件生成堆栈跟踪log得到的crashLog.txt文件如下
```
Operating system: Android
0.0.0 Linux 4.9.112-perf-gb92eddd #1 SMP PREEMPT Tue Jan 1 21:35:06 CST 2019 aarch64
CPU: arm64 // 注意点1
8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x600 // 注意点2
x0 = 0x00000078e0ce8460 x1 = 0x0000007fd4000314
x2 = 0x0000007fd40003b0 x3 = 0x00000078e0237134
x4 = 0x0000007fd40005d0 x5 = 0x00000078dca14200
x6 = 0x0000007fd4000160 x7 = 0x00000078c8987e18
x8 = 0x0000000000000000 x9 = 0x0000000000000001
x10 = 0x0000000000430000 x11 = 0x00000078e05ef688
x12 = 0x00000079664ab050 x13 = 0x0ad046ab5a65bfdf
x14 = 0x000000796650c000 x15 = 0xffffffffffffffff
x16 = 0x00000078c83defe8 x17 = 0x00000078c83ce5ec
x18 = 0x0000000000000001 x19 = 0x00000078e0c14c00
x20 = 0x0000000000000000 x21 = 0x00000078e0c14c00
x22 = 0x0000007fd40005e0 x23 = 0x00000078c89fa661
x24 = 0x0000000000000004 x25 = 0x00000079666cc5e0
x26 = 0x00000078e0c14ca0 x27 = 0x0000000000000001
x28 = 0x0000007fd4000310 fp = 0x0000007fd40002e0
lr = 0x00000078c83ce624 sp = 0x0000007fd40002c0
pc = 0x00000078c83ce600
Found by: given as instruction pointer in context
1 libcrash-lib.so + 0x620
fp = 0x0000007fd4000310 lr = 0x00000078e051c7e4
sp = 0x0000007fd40002f0 pc = 0x00000078c83ce624
Found by: previous frame's frame pointer
2 libart.so + 0x55f7e0
fp = 0x130c0cf800000001 lr = 0x00000079666cc5e0
sp = 0x0000007fd4000320 pc = 0x00000078e051c7e4
Found by: previous frame's frame pointer
......
```
下来是符号解析可以使用NDK中提供的`addr2line`来根据地址进行一个符号反解的过程,该工具在`$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line`
注意此处要注意一下平台如果是ARM 64位的so解析是需要使用aarch64-linux-android-4.9下的工具链。
因为我的是ARM 64位的so。所以使用aarch64-linux-android-4.9libcrash-lib.so在`app/build/intermediates/cmake/debug/obj/arm64-v8a`下,`0x600`为错误位置符号。
```
aarch64-linux-android-addr2line -f -C -e libcrash-lib.so 0x600
```
输出结果如下:
```
Crash()
/Users/weilu/Downloads/Chapter01-master/sample/.externalNativeBuild/cmake/debug/arm64-v8a/../../../../src/main/cpp/crash.cpp:10
```
可以看到输出结果与下图错误位置一致第10行
<img src="https://static001.geekbang.org/resource/image/ff/ab/ffbea53bc34d05c02055f1e348594dab.png" alt="">
[**Chapter02**](https://github.com/AndroidAdvanceWithGeektime/Chapter02)
>
该例子主要演示了如何通过关闭FinalizerWatchdogDaemon来减少TimeoutException的触发。
在我的上一篇博客:[安卓开发中遇到的奇奇怪怪的问题(三)](https://blog.csdn.net/qq_17766199/article/details/84789495#t1)中有说明,就不重复赘述了。
[**Chapter03**](https://github.com/AndroidAdvanceWithGeektime/Chapter03)
>
项目使用了Inline Hook来拦截内存对象分配时候的RecordAllocation函数通过拦截该接口可以快速获取到当时分配对象的类名和分配的内存大小。
在初始化的时候我们设置了一个分配对象数量的最大值如果从start开始对象分配数量超过最大值就会触发内存dump然后清空alloc对象列表重新计算。该功能和Android Studio里的Allocation Tracker类似只不过可以在代码级别更细粒度的进行控制。可以精确到方法级别。
项目直接跑起来后点击开始记录然后点击5次生成1000对象按钮。生成对象代码如下
```
for (int i = 0; i &lt; 1000; i++) {
Message msg = new Message();
msg.what = i;
}
```
因为代码从点击开始记录开始触发到5000的数据就dump到文件中点击5次后就会在`sdcard/crashDump`下生成一个时间戳命名的文件。项目根目录下调用命令:
```
java -jar tools/DumpPrinter-1.0.jar dump文件路径 &gt; dump_log.txt
```
然后就可以在dump_log.txt中看到解析出来的数据
```
Found 5000 records:
....
tid=4509 android.graphics.drawable.RippleForeground (112 bytes)
android.graphics.drawable.RippleDrawable.tryRippleEnter (RippleDrawable.java:569)
android.graphics.drawable.RippleDrawable.setRippleActive (RippleDrawable.java:276)
android.graphics.drawable.RippleDrawable.onStateChange (RippleDrawable.java:266)
android.graphics.drawable.Drawable.setState (Drawable.java:778)
android.view.View.drawableStateChanged (View.java:21137)
android.widget.TextView.drawableStateChanged (TextView.java:5289)
android.support.v7.widget.AppCompatButton.drawableStateChanged (AppCompatButton.java:155)
android.view.View.refreshDrawableState (View.java:21214)
android.view.View.setPressed (View.java:10583)
android.view.View.setPressed (View.java:10561)
android.view.View.onTouchEvent (View.java:13865)
android.widget.TextView.onTouchEvent (TextView.java:10070)
android.view.View.dispatchTouchEvent (View.java:12533)
android.view.ViewGroup.dispatchTransformedTouchEvent (ViewGroup.java:3032)
android.view.ViewGroup.dispatchTouchEvent (ViewGroup.java:2662)
android.view.ViewGroup.dispatchTransformedTouchEvent (ViewGroup.java:3032)
tid=4515 int[] (104 bytes)
tid=4509 android.os.BaseLooper$MessageMonitorInfo (88 bytes)
android.os.Message.&lt;init&gt; (Message.java:123)
com.dodola.alloctrack.MainActivity$4.onClick (MainActivity.java:70)
android.view.View.performClick (View.java:6614)
android.view.View.performClickInternal (View.java:6591)
android.view.View.access$3100 (View.java:786)
android.view.View$PerformClick.run (View.java:25948)
android.os.Handler.handleCallback (Handler.java:873)
android.os.Handler.dispatchMessage (Handler.java:99)
android.os.Looper.loop (Looper.java:201)
android.app.ActivityThread.main (ActivityThread.java:6806)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:547)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:873)
......
```
我们用Android Profiler查找一个Message对象对比一下一模一样。
<img src="https://static001.geekbang.org/resource/image/ea/10/ea9e03f90bc15a086d523958272fe410.png" alt="">
简单看一下Hook代码
```
void hookFunc() {
LOGI(&quot;start hookFunc&quot;);
void *handle = ndk_dlopen(&quot;libart.so&quot;, RTLD_LAZY | RTLD_GLOBAL);
if (!handle) {
LOGE(&quot;libart.so open fail&quot;);
return;
}
void *hookRecordAllocation26 = ndk_dlsym(handle,
&quot;_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj&quot;);
void *hookRecordAllocation24 = ndk_dlsym(handle,
&quot;_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj&quot;);
void *hookRecordAllocation23 = ndk_dlsym(handle,
&quot;_ZN3art3Dbg16RecordAllocationEPNS_6ThreadEPNS_6mirror5ClassEj&quot;);
void *hookRecordAllocation22 = ndk_dlsym(handle,
&quot;_ZN3art3Dbg16RecordAllocationEPNS_6mirror5ClassEj&quot;);
if (hookRecordAllocation26 != nullptr) {
LOGI(&quot;Finish get symbol26&quot;);
MSHookFunction(hookRecordAllocation26, (void *) &amp;newArtRecordAllocation26,
(void **) &amp;oldArtRecordAllocation26);
} else if (hookRecordAllocation24 != nullptr) {
LOGI(&quot;Finish get symbol24&quot;);
MSHookFunction(hookRecordAllocation26, (void *) &amp;newArtRecordAllocation26,
(void **) &amp;oldArtRecordAllocation26);
} else if (hookRecordAllocation23 != NULL) {
LOGI(&quot;Finish get symbol23&quot;);
MSHookFunction(hookRecordAllocation23, (void *) &amp;newArtRecordAllocation23,
(void **) &amp;oldArtRecordAllocation23);
} else {
LOGI(&quot;Finish get symbol22&quot;);
if (hookRecordAllocation22 == NULL) {
LOGI(&quot;error find hookRecordAllocation22&quot;);
return;
} else {
MSHookFunction(hookRecordAllocation22, (void *) &amp;newArtRecordAllocation22,
(void **) &amp;oldArtRecordAllocation22);
}
}
dlclose(handle);
}
```
使用了Inline Hook方案Substrate来拦截内存对象分配时候libart.so的RecordAllocation函数。首先如果我们要hook一个函数需要知道这个函数的地址。我们也看到了代码中这个地址判断了四种不同系统。这里有一个[网页版的解析工具](http://demangler.com/)可以快速获取。下面以8.0为例。
<img src="https://static001.geekbang.org/resource/image/89/08/89bc383aab02af0b71d243dc10273708.png" alt="">
我在8.0的源码中找到了对应的方法:
<img src="https://static001.geekbang.org/resource/image/a1/26/a1655f62725d8ae28a14ed717860e726.jpeg" alt="">
7.0方法就明显不同:
<img src="https://static001.geekbang.org/resource/image/3a/25/3a2e76c476c6a21e6036022793521925.jpeg" alt="">
我也同时参看了9.0的代码发现没有变化所以我的测试机是9.0的也没有问题。
Hook新内存对象分配处理代码
```
static bool newArtRecordAllocationDoing24(Class *type, size_t byte_count) {
allocObjectCount++;
//根据 class 获取类名
char *typeName = GetDescriptor(type, &amp;a);
//达到 max
if (allocObjectCount &gt; setAllocRecordMax) {
CMyLock lock(g_Lock);//此处需要 loc 因为对象分配的时候不知道在哪个线程,不 lock 会导致重复 dump
allocObjectCount = 0;
// dump alloc 里的对象转换成 byte 数据
jbyteArray allocData = getARTAllocationData();
// 将alloc数据写入文件
SaveAllocationData saveData{allocData};
saveARTAllocationData(saveData);
resetARTAllocRecord();
LOGI(&quot;===========CLEAR ALLOC MAPS=============&quot;);
lock.Unlock();
}
return true;
}
```
[**Chapter04**](https://github.com/AndroidAdvanceWithGeektime/Chapter04)
>
通过分析内存文件hprof快速判断内存中是否存在重复的图片并且将这些重复图片的PNG、堆栈等信息输出。
首先是获取我们需要分析的hprof文件我们加载两张相同的图片
```
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
imageView1.setImageBitmap(bitmap1);
imageView2.setImageBitmap(bitmap2);
```
生成hprof文件
```
// 手动触发GC
Runtime.getRuntime().gc();
System.runFinalization();
Debug.dumpHprofData(file.getAbsolutePath());
```
接下来就是利用[HAHA库](https://github.com/square/haha)进行文件分析的核心代码:
```
// 打开hprof文件
final HeapSnapshot heapSnapshot = new HeapSnapshot(hprofFile);
// 获得snapshot
final Snapshot snapshot = heapSnapshot.getSnapshot();
// 获得Bitmap Class
final ClassObj bitmapClass = snapshot.findClass(&quot;android.graphics.Bitmap&quot;);
// 获得heap, 只需要分析app和default heap即可
Collection&lt;Heap&gt; heaps = snapshot.getHeaps();
for (Heap heap : heaps) {
// 只需要分析app和default heap即可
if (!heap.getName().equals(&quot;app&quot;) &amp;&amp; !heap.getName().equals(&quot;default&quot;)) {
continue;
}
for (ClassObj clazz : bitmapClasses) {
//从heap中获得所有的Bitmap实例
List&lt;Instance&gt; bitmapInstances = clazz.getHeapInstances(heap.getId());
//从Bitmap实例中获得buffer数组,宽高信息等。
ArrayInstance buffer = HahaHelper.fieldValue(((ClassInstance) bitmapInstance).getValues(), &quot;mBuffer&quot;);
int bitmapHeight = fieldValue(bitmapInstance, &quot;mHeight&quot;);
int bitmapWidth = fieldValue(bitmapInstance, &quot;mWidth&quot;);
// 引用链信息
while (bitmapInstance.getNextInstanceToGcRoot() != null) {
print(instance.getNextInstanceToGcRoot());
instance = instance.getNextInstanceToGcRoot();
}
// 根据hashcode来进行重复判断
}
}
```
最终的输出结果:
<img src="https://static001.geekbang.org/resource/image/96/9c/963f4a3cabf8ef5aa4faa0a61c55bd9c.jpeg" alt="">
我们用Studio打开hprof文件对比一下
<img src="https://static001.geekbang.org/resource/image/04/55/049c6beae610971175f7521bcf4f8b55.jpeg" alt="">
可以看到信息是一摸一样的。对于更优处理引用链的信息,可以参看[LeakCanary](https://github.com/square/leakcanary)源码的实现。
我已经将上面的代码打成JAR包可以直接调用
```
//调用方法:
java -jar tools/DuplicatedBitmapAnalyzer-1.0.jar hprof文件路径
```
详细的代码我提交到了[Github](https://github.com/simplezhli/Chapter04),供大家参考。
[**Chapter05**](https://github.com/AndroidAdvanceWithGeektime/Chapter05)
>
尝试模仿[ProcessCpuTracker.java](http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/com/android/internal/os/ProcessCpuTracker.java)拿到一段时间内各个线程的耗时占比。
```
usage: CPU usage 5000ms(from 23:23:33.000 to 23:23:38.000):
System TOTAL: 2.1% user + 16% kernel + 9.2% iowait + 0.2% irq + 0.1% softirq + 72% idle
CPU Core: 8
Load Average: 8.74 / 7.74 / 7.36
Process:com.sample.app
50% 23468/com.sample.app(S): 11% user + 38% kernel faults:4965
Threads:
43% 23493/singleThread(R): 6.5% user + 36% kernel faults3094
3.2% 23485/RenderThread(S): 2.1% user + 1% kernel faults329
0.3% 23468/.sample.app(S): 0.3% user + 0% kernel faults6
0.3% 23479/HeapTaskDaemon(S): 0.3% user + 0% kernel faults982
...
```
因为了解Linux不多所以看这个有点懵逼。好在课代表孙鹏飞同学解答了相关问题看懂了上面信息同时学习到了一些Linux知识。
```
private void testIO() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
File f = new File(getFilesDir(), &quot;aee.txt&quot;);
FileOutputStream fos = new FileOutputStream(f);
byte[] data = new byte[1024 * 4 * 3000];// 此处分配一个 12mb 大小的 byte 数组
for (int i = 0; i &lt; 30; i++) {// 由于 IO cache 机制的原因所以此处写入多次 cache触发 dirty writeback 到磁盘中
Arrays.fill(data, (byte) i);// 当执行到此处的时候产生 minor fault并且产生 User cpu useage
fos.write(data);
}
fos.flush();
fos.close();
}
});
thread.setName(&quot;SingleThread&quot;);
thread.start();
}
```
上述代码就是导致的问题罪魁祸首这种密集I/O操作集中在SingleThread线程中处理导致发生了3094次faults、36% kernel完全没有很好利用到8核CPU。
最后通过检测CPU的使用率可以更好地避免卡顿现象防止ANR的发生。
前前后后用了两三天的时间,远远没有当初想的顺利,感觉身体被掏空。中间也爬了不少坑,虽然没有太深入实现代码,但是中间的体验过程也是收获不小。所以总不能因为难就放弃了,先做到力所能及的部分,让自己动起来!
**参考**
<li>
[练习Sample跑起来 | 热点问题答疑第1期](https://time.geekbang.org/column/article/73068)
</li>
<li>
[练习Sample跑起来 | 热点问题答疑第2期](https://time.geekbang.org/column/article/75440)
</li>

View File

@@ -0,0 +1,318 @@
<audio id="audio" title="练习Sample跑起来 | 唯鹿同学的练习手记 第2辑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8b/be/8b7f5c322632493d7d6e9729188cffbe.mp3"></audio>
你好,我是唯鹿。
接着上篇[练习手记](https://time.geekbang.org/column/article/83742)今天练习68、12、17、19这六期内容主要针对有课后Sample练习的相比15期轻松了很多。
[**Chapter06**](https://github.com/AndroidAdvanceWithGeektime/Chapter06)
>
该项目展示了使用PLT Hook技术来获取Atrace的日志可以学习到systrace的一些底层机制。
没有什么问题项目直接可以运行起来。运行项目后点击开启Atrace日志然后就可以在Logcat日志中查看到捕获的日志如下
```
11:40:07.031 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= install systrace hoook =========
11:40:07.034 8537-8537/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|Record View#draw()
11:40:07.034 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|DrawFrame
11:40:07.035 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|syncFrameState
========= B|8537|prepareTree
========= E
========= E
========= B|8537|eglBeginFrame
========= E
========= B|8537|computeOrdering
========= E
========= B|8537|flush drawing commands
========= E
11:40:07.036 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|eglSwapBuffersWithDamageKHR
========= B|8537|setSurfaceDamage
========= E
11:40:07.042 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|queueBuffer
========= E
11:40:07.043 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|dequeueBuffer
========= E
========= E
========= E
```
通过B|事件和E|事件是成对出现的这样就可以计算出应用执行每个事件使用的时间。那么上面的Log中View的draw()方法显示使用了9ms。
这里实现方法是使用了[Profilo](https://github.com/facebookincubator/profilo)的PLT Hook来hook libc.so的`write``__write_chk`方法。libc是C的基础库函数为什么要hook这些方法需要我们补充C、Linux相关知识。
同理[Chapter06-plus](https://github.com/AndroidAdvanceWithGeektime/Chapter06-plus)展示了如何使用 PLT Hook技术来获取线程创建的堆栈README有详细的实现步骤介绍我就不赘述了。
[**Chapter07**](https://github.com/AndroidAdvanceWithGeektime/Chapter07)
>
这个Sample是学习如何给代码加入Trace Tag大家可以将这个代码运用到自己的项目中然后利用systrace查看结果。这就是所谓的systrace + 函数插桩。
操作步骤:
<li>
使用Android Studio打开工程Chapter07。
</li>
<li>
运行Gradle Task `:systrace-gradle-plugin:buildAndPublishToLocalMaven`编译plugin插件。
</li>
<li>
使用Android Studio单独打开工程systrace-sample-android。
</li>
<li>
编译运行App插桩后的class文件在目录`Chapter07/systrace-sample-android/app/build/systrace_output/classes`中查看)。
</li>
对比一下插桩效果,插桩前:
<img src="https://static001.geekbang.org/resource/image/65/a5/6587c2a9e0d4cae3b5336cbf9ef91da5.jpeg" alt="">
插桩后:
<img src="https://static001.geekbang.org/resource/image/08/13/0895158565b8548d86d2e076325c0713.jpeg" alt="">
可以看到在方法执行前后插入了TraceTag这样的话`beginSection`方法和`endSection`方法之间的代码就会被追踪。
```
public class TraceTag {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public static void i(String name) {
Trace.beginSection(name);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public static void o() {
Trace.endSection();
}
```
其实Support-Compat库中也有类似的一个[TraceCompat](https://developer.android.google.cn/reference/android/support/v4/os/TraceCompat),项目中可以直接使用。
然后运行项目打开systrace
```
python $ANDROID_HOME/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a com.sample.systrace -o test.log.html
```
<img src="https://static001.geekbang.org/resource/image/d1/0e/d17230ca7e70b399101b1ce66dd35e0e.jpeg" alt="">
最后打开生成的test.log.html文件就可以查看systrace记录
<img src="https://static001.geekbang.org/resource/image/e0/4b/e093fc650bfeafd053eb845a1b1d9c4b.jpeg" alt="">
当然这一步我们也可以使用SDK中的Monitor效果是一样的。
使用systrace + 函数插桩的方式我们就可以很方便地观察每个方法的耗时从而针对耗时的方法进行优化尤其是Application的启动优化。
[**Chapter08**](https://github.com/AndroidAdvanceWithGeektime/Chapter08)
>
该项目展示了关闭掉虚拟机的class verify后对性能的影响。
在加载类的过程有一个verify class的步骤它需要校验方法的每一个指令是一个比较耗时的操作。这个例子就是通过Hook去掉verify这个步骤。该例子尽量在Dalvik下执行在ART下的效果并不明显。
去除校验代码(可以参看阿里的[Atlas](https://github.com/alibaba/atlas)
```
AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(this.getApplicationContext(), true);
runtime.setVerificationEnabled(false);
```
具体运行效果这里我就不展示了,直接运行体验就可以了。
[**Chapter12**](https://github.com/AndroidAdvanceWithGeektime/Chapter12)
>
通过复写Application的`getSharedPreferences`替换系统`SharedPreferences`的实现核心的优化在于修改了Apply的实现将多个Apply方法在内存中合并而不是多次提交。
修改`SharedPreferencesImpl`的Apply部分如下
```
public void apply() {
// 先调用commitToMemory()
final MemoryCommitResult mcr = commitToMemory();
boolean hasDiskWritesInFlight = false;
synchronized (SharedPreferencesImpl.this) {
// mDiskWritesInFlight大于0说明之前已经有调用过commitToMemory()了
hasDiskWritesInFlight = mDiskWritesInFlight &gt; 0;
}
// 源码没有这层判断,直接提交。
if (!hasDiskWritesInFlight) {
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
```
[**Chapter14**](https://github.com/AndroidAdvanceWithGeektime/Chapter14)
这个是全面解析SQLite的资料有兴趣的可以下载看看。
[**Chapter17**](https://github.com/AndroidAdvanceWithGeektime/Chapter17)
>
该项目展示了如何使用PLT Hook技术来获取网络请求相关信息。
通过PLT Hook代理Socket相关的几个重要函数
```
/**
* 直接 hook 内存中的所有so但是需要排除掉socket相关方法本身定义的libc(不然会出现循坏)
* plt hook
*/
void hookLoadedLibs() {
ALOG(&quot;hook_plt_method&quot;);
hook_plt_method_all_lib(&quot;libc.so&quot;, &quot;send&quot;, (hook_func) &amp;socket_send_hook);
hook_plt_method_all_lib(&quot;libc.so&quot;, &quot;recv&quot;, (hook_func) &amp;socket_recv_hook);
hook_plt_method_all_lib(&quot;libc.so&quot;, &quot;sendto&quot;, (hook_func) &amp;socket_sendto_hook);
hook_plt_method_all_lib(&quot;libc.so&quot;, &quot;recvfrom&quot;, (hook_func) &amp;socket_recvfrom_hook);
hook_plt_method_all_lib(&quot;libc.so&quot;, &quot;connect&quot;, (hook_func) &amp;socket_connect_hook);
}
```
```
int hook_plt_method_all_lib(const char* exclueLibname, const char* name, hook_func hook) {
if (refresh_shared_libs()) {
// Could not properly refresh the cache of shared library data
return -1;
}
int failures = 0;
for (auto const&amp; lib : allSharedLibs()) {
if (strcmp(lib.first.c_str(), exclueLibname) != 0) {
failures += hook_plt_method(lib.first.c_str(), name, hook);
}
}
return failures;
}
```
运行项目,访问百度的域名[https://www.baidu.com](https://www.baidu.com),输出如下:
```
17:08:28.347 12145-12163/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 10
17:08:28.349 12145-12163/com.dodola.socket E/HOOOOOOOOK: stack:com.dodola.socket.SocketHook.getStack(SocketHook.java:13)
java.net.PlainSocketImpl.socketConnect(Native Method)
java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:334)
java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:196)
java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:178)
java.net.SocksSocketImpl.connect(SocksSocketImpl.java:356)
java.net.Socket.connect(Socket.java:586)
com.android.okhttp.internal.Platform.connectSocket(Platform.java:113)
com.android.okhttp.Connection.connectSocket(Connection.java:196)
com.android.okhttp.Connection.connect(Connection.java:172)
com.android.okhttp.Connection.connectAndSetOwner(Connection.java:367)
com.android.okhttp.OkHttpClient$1.connectAndSetOwner(OkHttpClient.java:130)
com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:329)
com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:246)
com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnection
AF_INET6 ipv6 IP===&gt;183.232.231.173:443
socket_connect_hook sa_family: 1
Ignore local socket connect
02-07 17:08:28.637 12145-12163/com.dodola.socket E/HOOOOOOOOK: respond:&lt;!DOCTYPE html&gt;
&lt;html&gt;&lt;!--STATUS OK--&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;title&gt;百度一下,你就知道&lt;/title&gt;
```
可以看到我们获取到了网络请求的相关信息。
最后我们可以通过Connect函数的hook实现很多需求例如
- 禁用应用网络访问
- 过滤广告IP
- 禁用定位功能
[**Chapter19**](https://github.com/AndroidAdvanceWithGeektime/Chapter19)
>
使用Java Hook实现Alarm、WakeLock与GPS的耗电监控。
实现原理
根据老师提供的提示信息,动态代理对应的[PowerManager](http://androidxref.com/7.0.0_r1/xref/frameworks/base/core/java/android/os/PowerManager.java)、[AlarmManager](http://androidxref.com/7.0.0_r1/xref/frameworks/base/core/java/android/app/AlarmManager.java)、[LocationManager](http://androidxref.com/7.0.0_r1/xref/frameworks/base/location/java/android/location/LocationManager.java)的`mService`实现,要拦截的方法在[PowerManagerService](http://androidxref.com/7.0.0_r1/xref/frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java)、[AlarmManagerService](http://androidxref.com/7.0.0_r1/xref/frameworks/base/services/core/java/com/android/server/AlarmManagerService.java)、[LocationManagerService](http://androidxref.com/7.0.0_r1/xref/frameworks/base/services/core/java/com/android/server/LocationManagerService.java)中。
实现核心代码:
```
Object oldObj = mHostContext.getSystemService(Context.XXX_SERVICE);
Class&lt;?&gt; clazz = oldObj.getClass();
Field field = clazz.getDeclaredField(&quot;mService&quot;);
field.setAccessible(true);
final Object mService = field.get(oldObj);
setProxyObj(mService);
Object newObj = Proxy.newProxyInstance(this.getClass().getClassLoader(), mService.getClass().getInterfaces(), this);
field.set(oldObj, newObj)
```
写了几个调用方法去触发,通过判断对应的方法名来做堆栈信息的输出。
输出的堆栈信息如下:
<img src="https://static001.geekbang.org/resource/image/43/b7/43e081eacdef050b78b71043c87acdb7.png" alt="">
当然强大的Studio在3.2后也有了强大的耗电量分析器同样可以监测到这些信息如下图所示我使用的Studio版本为3.3)。
<img src="https://static001.geekbang.org/resource/image/82/3d/82b2939991e4c9c729017255fb1cb73d.png" alt="">
实现不足之处:
- 可能兼容性上不是特别完善(期待老师的标准答案)。
- 没有按照耗电监控的规则去做一些业务处理。
心得体会:
- 本身并不复杂只是为了找到Hook点看了对应的Service源码耗费了一些时间对于它们的工作流程有了更深的认识。
- 平时也很少使用动态代理,这回查漏补缺,一次用了个爽。
这个作业前前后后用了一天时间之前作业还有一些同学提供PR所以相对轻松些但这次没有参考走了点弯路不过收获也是巨大的。我就不细说了感兴趣的话可以参考我的实现。完整代码参见[GitHub](https://github.com/simplezhli/Chapter19),仅供参考。
**参考**
- [练习Sample跑起来 | 热点问题答疑第3期](https://time.geekbang.org/column/article/76413)
- [练习Sample跑起来 | 热点问题答疑第4期](https://time.geekbang.org/column/article/79331)

View File

@@ -0,0 +1,354 @@
<audio id="audio" title="练习Sample跑起来 | 唯鹿同学的练习手记 第3辑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/df/c2/df08966dbbe1afcf86c3debc177b82c2.mp3"></audio>
没想到之前的写的练习心得得到了老师的认可看来我要更加认真努力练习了。今天来练习第22、27、ASM这三课的Sample。
[**Chapter22**](https://github.com/AndroidAdvanceWithGeektime/Chapter22)
>
尝试使用Facebook ReDex库来优化我们的安装包。
**准备工作**
首先是下载ReDex
```
git clone https://github.com/facebook/redex.git
cd redex
```
接着是安装:
```
autoreconf -ivf &amp;&amp; ./configure &amp;&amp; make -j4
sudo make install
```
在安装时执行到这里,报出下图错误:
<img src="https://static001.geekbang.org/resource/image/40/fa/40ba14544153f1ef67bfd21a884c1efa.jpg" alt="">
其实就是没有安装Boost所以执行下面的命令安装它。
```
brew install boost jsoncpp
```
安装Boost完成后再等待十几分钟时间安装ReDex。
下来就是编译我们的Sample得到的安装包信息如下。
<img src="https://static001.geekbang.org/resource/image/bc/0b/bcf38372f4d9315b9d288607e437040b.jpeg" alt="">
可以看到有三个Dex文件APK大小为13.7MB。
**通过ReDex命令优化**
为了让我们可以更加清楚流程你可以输出ReDex的日志。
```
export TRACE=2
```
去除Debuginfo的方法需要在项目根目录执行
```
redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android -c redex-test/stripdebuginfo.config -P ReDexSample/proguard-rules.pro -o redex-test/strip_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk
```
上面这段很长的命令,其实可以拆解为几部分:
<li>
`--sign` 签名信息
</li>
<li>
`-s`keystore签名文件路径
</li>
<li>
`-a`keyalias签名的别名
</li>
<li>
`-p`keypass签名的密码
</li>
<li>
`-c` 指定ReDex的配置文件路径
</li>
<li>
`-P` ProGuard规则文件路径
</li>
<li>
`-o` 输出的文件路径
</li>
<li>
最后是要处理APK文件的路径
</li>
但在使用时,我遇到了下图的问题:
<img src="https://static001.geekbang.org/resource/image/f9/42/f942ef115b2293562b6c3d533c0abd42.png" alt="">
这里是找不到`Zipalign`所以需要我们配置Android SDK的根目录路径添加在原命令前面
```
ANDROID_SDK=/path/to/android/sdk redex [... arguments ...]
```
结果如下:
<img src="https://static001.geekbang.org/resource/image/4f/28/4f442a95f1518cbe38311b042cdda028.png" alt="">
实际的优化效果是原Debug包为14.21MB去除Debuginfo的方法后为12.91MB,效果还是不错的。**去除的内容就是一些调试信息及堆栈行号。**
<img src="https://static001.geekbang.org/resource/image/fd/07/fda8e0b637df6f145f9867764720ab07.jpeg" alt="">
不过老师在Sample的proguard-rules.pro中添加了`-keepattributes SourceFile,LineNumberTable`保留了行号信息。
所以处理后的包安装后进入首页,还是可以看到堆栈信息的行号。
**Dex重分包的方法**
```
redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android -c redex-test/interdex.config -P ReDexSample/proguard-rules.pro -o redex-test/interdex_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk
```
和之前的命令一样,只是`-c`使用的配置文件为interdex.config。
输出信息:
<img src="https://static001.geekbang.org/resource/image/29/aa/293f13ab6fe75ede7d4840d04f0d56aa.jpeg" alt="">
优化效果为原Debug包为14.21MB、3个Dex优化后为13.34MB、2个Dex。
<img src="https://static001.geekbang.org/resource/image/77/c3/77abb69a81448e677b64bb5cbd59fec3.jpeg" alt="">
根据老师的介绍,**如果你的应用有4个以上的Dex这个体积优化至少有10%**。 看来效果还是很棒棒的。至于其他问题比如在Windows环境使用ReDex可以参看ReDex的[使用文档](https://fbredex.com/docs/installation)。
[**Chapter27**](https://github.com/AndroidAdvanceWithGeektime/Chapter27)
>
利用AspectJ实现插桩的例子。
效果和[Chapter07](https://github.com/AndroidAdvanceWithGeektime/Chapter07)是一样的只是Chapter07使用的是ASM方式实现的这次是AspectJ实现。ASM与AspectJ都是Java字节码处理框架相比较来说AspectJ使用更加简单同样的功能实现只需下面这点代码但是ASM比AspectJ更加高效和灵活。
AspectJ实现代码
```
@Aspect
public class TraceTagAspectj {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@Before(&quot;execution(* **(..))&quot;)
public void before(JoinPoint joinPoint) {
Trace.beginSection(joinPoint.getSignature().toString());
}
/**
* hook method when it's called out.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@After(&quot;execution(* **(..))&quot;)
public void after() {
Trace.endSection();
}
```
简单介绍下上面代码的意思:
<li>
`@Aspect`在编译时AspectJ会查找被`@Aspect`注解的类然后执行我们的AOP实现。
</li>
<li>
`@Before`:可以简单理解为方法执行前。
</li>
<li>
`@After`:可以简单理解为方法执行后。
</li>
<li>
`execution`:方法执行。
</li>
<li>
`* **(..)`第一个星号代表任意返回类型第二个星号代表任意类第三个代表任意方法括号内为方法参数无限制。星号和括号内都是可以替换为具体值比如String TestClass.test(String)。
</li>
知道了相关注解的含义,那么实现的代码含义就是,**所有方法在执行前后插入相应指定操作**。
效果对比如下:
<img src="https://static001.geekbang.org/resource/image/64/77/644381974bcd1e3b2d468cdeb432ed77.png" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/02/ca/02b99a9e7fd70da8d9fdf086f31c78ca.png" alt="">
下来实现给MainActivity的`onResume`方法增加try catch。
```
@Aspect
public class TryCatchAspect {
@Pointcut(&quot;execution(* com.sample.systrace.MainActivity.onResume())&quot;) // &lt;- 指定类与方法
public void methodTryCatch() {
}
@Around(&quot;methodTryCatch()&quot;)
public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
// try catch
try {
joinPoint.proceed(); // &lt;- 调用原方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
上面用到了两个新注解:
<li>
`@Around`用于替换以前的代码使用joinPoint.proceed()可以调用原方法。
</li>
<li>
`@Pointcut`:指定一个切入点。
</li>
实现就是指定一个切入点利用替换原方法的思路包裹一层try catch。
效果对比如下:
<img src="https://static001.geekbang.org/resource/image/7f/c0/7f4a5bb6995c53872966c956d7e78ec0.png" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/08/bc/08d123aa792c8f4fc8538fd5658cb9bc.png" alt="">
当然AspectJ还有很多用法Sample中包含有《AspectJ程序设计指南》便于我们具体了解和学习AspectJ。
[**Chapter-ASM**](https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM)
>
Sample利用ASM实现了统计方法耗时和替换项目中所有的new Thread。
<li>
运行项目首先要注掉ASMSample build.gradle的`apply plugin: 'com.geektime.asm-plugin'`和根目录build.gradle的`classpath ("com.geektime.asm:asm-gradle-plugin:1.0") { changing = true }`
</li>
<li>
运行`gradle task ":asm-gradle-plugin:buildAndPublishToLocalMaven"`编译plugin插件编译的插件在本地`.m2\repository`目录下
</li>
<li>
打开第一步注掉的内容就可以运行了。
</li>
实现的大致过程是先利用Transform遍历所有文件再通过ASM的`visitMethod`遍历所有方法最后通过AdviceAdapter实现最终的修改字节码。具体实现可以看代码和[《练习Sample跑起来 | ASM插桩强化练习》](https://time.geekbang.org/column/article/83148)。
效果对比:
<img src="https://static001.geekbang.org/resource/image/ee/0b/ee98c9349e62d5aca66b883a89cd470b.png" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/d0/3a/d0dd3c68ac2d56b6eebf6853f871c43a.png" alt="">
下面是两个练习:
1.给某个方法增加try catch
这里我就给MainActivity的`mm`方法进行try catch。实现很简单直接修改ASMCode的TraceMethodAdapter。
```
public static class TraceMethodAdapter extends AdviceAdapter {
private final String methodName;
private final String className;
private final Label tryStart = new Label();
private final Label tryEnd = new Label();
private final Label catchStart = new Label();
private final Label catchEnd = new Label();
protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}
@Override
protected void onMethodEnter() {
if (className.equals(&quot;com/sample/asm/MainActivity&quot;) &amp;&amp; methodName.equals(&quot;mm&quot;)) {
mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, &quot;java/lang/Exception&quot;);
mv.visitLabel(tryStart);
}
}
@Override
protected void onMethodExit(int opcode) {
if (className.equals(&quot;com/sample/asm/MainActivity&quot;) &amp;&amp; methodName.equals(&quot;mm&quot;)) {
mv.visitLabel(tryEnd);
mv.visitJumpInsn(GOTO, catchEnd);
mv.visitLabel(catchStart);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, &quot;java/lang/RuntimeException&quot;, &quot;printStackTrace&quot;, &quot;()V&quot;, false);
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(catchEnd);
}
}
```
`visitTryCatchBlock`方法前三个参数均是Label实例其中一、二表示try块的范围三则是catch块的开始位置第四个参数是异常类型。其他的方法及参数就不细说了具体你可以参考[ASM文档](https://asm.ow2.io/asm4-guide.pdf)。
实现类似AspectJ在方法执行开始及结束时插入我们的代码。
效果我就不截图了,代码如下:
```
public void mm() {
try {
A a = new A(new B(2));
} catch (Exception e) {
e.printStackTrace();
}
}
```
2.查看代码中谁获取了IMEI
这个就更简单了直接寻找谁使用了TelephonyManager的`getDeviceId`方法并且在Sample中有答案。
```
public class IMEIMethodAdapter extends AdviceAdapter {
private final String methodName;
private final String className;
protected IMEIMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
if (owner.equals(&quot;android/telephony/TelephonyManager&quot;) &amp;&amp; name.equals(&quot;getDeviceId&quot;) &amp;&amp; desc.equals(&quot;()Ljava/lang/String;&quot;)) {
Log.e(&quot;asmcode&quot;, &quot;get imei className:%s, method:%s, name:%s&quot;, className, methodName, name);
}
}
}
```
Build后输出如下
<img src="https://static001.geekbang.org/resource/image/2d/94/2d5c01eee4fc651b5831c0341d6e0994.png" alt="">
总体来说ASM的上手难度还是高于AspectJ需要我们了解编译后的字节码这里所使用的功能也只是冰山一角。课代表鹏飞同学推荐的ASM Bytecode Outline插件是个好帮手最后我将我练习的代码也上传到了[GitHub](https://github.com/simplezhli/Chapter-ASM)里面还包括一份中文版的ASM文档有兴趣的同学可以下载看看。
参考
- [练习Sample跑起来 | ASM插桩强化练](http://time.geekbang.org/column/article/83148)
- [ASM文档](http://asm.ow2.io/asm4-guide.pdf)

View File

@@ -0,0 +1,197 @@
<audio id="audio" title="练习Sample跑起来 | 热点问题答疑第1期" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/38/037cbed0bc2bbf027adea3817931a538.mp3"></audio>
你好,我是专栏的“学习委员”孙鹏飞。
专栏上线以来很多同学反馈说在运行练习Sample的时候遇到问题。由于这些Sample多是采用C/C++来完成的所以在编译运行上会比传统的纯Java项目稍微复杂一些。今天我就针对第1期第4期中同学们集中遇到的问题做一期答疑。设置练习的目的也是希望你在学习完专栏的内容后可以快速上手试验一下专栏所讲的工具或方法帮你加快掌握技术的精髓。所以希望各位同学可以多参与进来有任何问题也可以在留言区给我们反馈后面我还会不定期针对练习再做答疑。
## 编译环境配置
首先是同学们问得比较多的运行环境问题。
前几期的练习Sample大多是使用C/C++开发的所以要运行起来需要先配置好SDK和NDKSDK我们一般都是配置好的NDK环境的配置有一些特殊的地方一般我们的Sample都会使用最新的NDK版本代码可能会使用C++11/14的语法进行编写并且使用CMake进行编译我这里给出NDK环境的配置项。
首先需要去NDK官网下载[最新版本](http://developer.android.com/ndk/downloads/)下载后可以解压到合适的地方一般macOS可以存放在 ANDROID_SDK_HOME/ndk_bundle目录下Android Studio可以默认找到该目录。如果放到别的目录可能需要自己指定一下。
指定NDK目录的方法一般有下面两种。
1.在练习Sample根目录下都会有一个local.properties文件修改其中的ndk.dir路径即可。
```
ndk.dir=/Users/sample/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/sample/Library/Android/sdk
```
2.可以在Android Studio里进行配置打开File -&gt; Project Structure -&gt; SDK Location进行修改。
<img src="https://static001.geekbang.org/resource/image/fb/18/fbbd0e4051c08b39891ab3483251f618.png" alt="">
上面两种修改方法效果是一致的。
有些Sample需要降级NDK编译使用可能需要下载旧版本的NDK可以从[官网下载](http://developer.android.com/ndk/downloads/revision_history)。
之后需要安装CMake和LLDB。
<li>
[CMake](http://cmake.org/)一款外部构建工具可与Gradle搭配使用来构建原生库。
</li>
<li>
[LLDB](http://lldb.llvm.org/)一种调试程序Android Studio使用它来[调试原生代码](http://developer.android.com/studio/debug/index.html?hl=zh-cn)。
</li>
**这两项都可以在Tools &gt; Android &gt; SDK Manager里进行安装**
<img src="https://static001.geekbang.org/resource/image/11/3c/119fd82b78608e6600fb8cac5275653c.png" alt="">
这样我们编译所需要的环境就配置好了。
## 热点问题答疑
[01 | 崩溃优化(上):关于“崩溃”那些事儿](http://time.geekbang.org/column/article/70602)
关于第1期的Sample同学们遇到的最多的问题是**使用模拟器运行无法获取Crash日志的问题**。
引起这个问题的缘由比较深层最直观的原因是使用Clang来编译x86平台下的Breakpad会导致运行出现异常从而导致无法抓取日志。想要解决这个问题我们需要先来了解一下NDK集成的编译器。
NDK集成了两套编译器GCC和Clang。从NDK r11开始官方就建议使用Clang详情可以看[ChangeLog](http://link.zhihu.com/?target=https%3A//github.com/android-ndk/ndk/wiki/Changelog-r11)并且标记GCC为Deprecated并且从GCC 4.8升级到4.9以后就不再进行更新了。NDK r13开始默认使用Clang。NDK r16b以后的版本貌似强制开启GCC会引起错误并将libc++作为默认的STL而NDK r18干脆就完全删除了GCC。
由于Clang的编译会引起x86的Breakpad执行异常所以我们需要切换到GCC下进行编译步骤如下。
1.首先将NDK切换到r16b你可以从[这里](http://developer.android.com/ndk/downloads/older_releases?hl=zh-cn)下载在里面找到对应你操作系统平台的NDK版本。
2.在Android Studio里设置NDK路径为ndk-16b的路径。
3.在练习例子源码的sample和breakpad-build的build.gradle配置里进行如下配置。
```
externalNativeBuild {
cmake {
cppFlags &quot;-std=c++11&quot;
arguments &quot;-DANDROID_TOOLCHAIN=gcc&quot;
}
}
```
第二个问题是**日志解析工具如何获取**。
解析Minidump日志主要是使用**minidump_stackwalk**工具,配合使用的工具是**dump_syms**这个工具可以获取一个so文件的符号表。
这两项工具需要通过编译Breakpad来获取有部分同学查到的文章是采用Chrome团队的[depot_tools](http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html)来进行工具的源码下载、编译操作。depot_tools是个很好用的工具但是在国内其服务器是无法访问的所以我们采用直接下载源码编译的方式相对来说比较方便。
编译Breakpad有一些需要注意的地方由于Android平台的内核是LinuxAndroid里的动态链接库的符号表导出工具**dump_syms**需要运行在Linux下暂时没有找到交叉编译在别的平台上的办法所以下面的步骤都是在Linux环境Ubuntu 18.04)下进行的,步骤如下。
1.先下载[源码](http://github.com/google/breakpad)。
2.由于源码里没有附带上一些第三方的库所以现在编译会出现异常我们需要下载lss库到Breakpad源码目录src/third_party下面。
```
git clone https://chromium.googlesource.com/linux-syscall-support
```
3.然后在源码目录下执行。
```
./configure &amp;&amp; make
make install
```
这样我们就可以直接调用**minidump_stackwalk、dump_syms**工具了。
第三个问题是**如何解析抓取下来的Minidump日志**。
生成的Crash信息如果授予Sdcard权限会优先存放在/sdcard/crashDump下便于我们做进一步的分析。反之会放到目录/data/data/com.dodola.breakpad/files/crashDump下。
你可以通过adb pull命令拉取日志文件。
```
adb pull /sdcard/crashDump/
```
1.首先我们需要从产生Crash的动态库中提取出符号表以第1期的Sample为例产生Crash的动态库obj路径在**Chapter01/sample/build/intermediates/cmake/debug/obj下**。
<img src="https://static001.geekbang.org/resource/image/83/e1/83d31e7582755c8486bf43e01ece48e1.png" alt="">
此处需要注意一下手机平台按照运行Sample时的平台取出libcrash-lib.so库进行符号表的dump然后调用dump_syms工具获取符号表。
```
dump_syms libcrash-lib.so &gt; libcrash-lib.so.sym
```
2.建立符号表目录结构。首先打开刚才生成的libcrash-lib.so.syms找到如下编码。
```
MODULE Linux arm64 322FCC26DA8ED4D7676BD9A174C299630 libcrash-lib.so
```
然后建立如下结构的目录Symbol/libcrash-lib.so/322FCC26DA8ED4D7676BD9A174C299630/将libcrash-lib.so.sym文件复制到该文件夹中。注意目录结构不能有错否则会导致符号表对应失败。
3.完成上面的步骤后就可以来解析Crash日志了执行minidump_stackwalk命令。
```
minidump_stackwalk crash.dmp ./Symbol &gt; dump.txt
```
4.这样我们获取的crash日志就会有符号表了对应一下之前没有符号表时候的日志记录。
<img src="https://static001.geekbang.org/resource/image/25/c7/252da4578624d2a85420dfb294f0efc7.png" alt="">
5.如果我们没有原始的obj那么需要通过libcrash-lib.so的导出符号来进行解析这里用到的工具是addr2line工具这个工具存放在$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line下。你要注意一下平台如果是解析64位的动态库需要使用aarch64-linux-android-4.9下的addr2line此处是64位的
```
aarch64-linux-android-addr2line -f -C -e libcrash-lib.so 0x5f8
Java_com_dodola_breakpad_MainActivity_crash
```
6.可以使用GDB来根据Minidump调试出问题的动态库这里就不展开了你可以参考[这里](http://www.chromium.org/chromium-os/packages/crash-reporting/debugging-a-minidump)。
[03 | 内存优化4GB内存时代再谈内存优化](http://time.geekbang.org/column/article/71277)
针对这一期的Sample很多同学询问Sample中经常使用的Hook框架的原理。
Sample中使用的Hook框架有两种一种是Inline Hook方案[Substrate](http://github.com/AndroidAdvanceWithGeektime/Chapter03/tree/master/alloctrackSample/src/main/cpp/Substrate)和[HookZz](http://github.com/jmpews/HookZz)一种是PLT Hook方案[Facebook Hook](http://github.com/facebookincubator/profilo/tree/master/deps/linker)),这两种方案各有优缺点,根据要实现功能的不同采取不同的框架。
PLT Hook相对Inline Hook的方案要稳定很多但是它操作的范围只是针对出现在PLT表中的动态链接函数而Inline Hook可以hook整个so里的所有代码。Inline Hook由于要针对各个平台进行指令修复操作所以稳定性和兼容性要比PLT Hook差很多。
关于PLT Hook的内容你可以看一下《程序员的自我修养链接、装载与库》这本书而Inline Hook则需要对ARM、x86汇编以及各个平台下的过程调用标准[Procedure Call Standard](http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042f/IHI0042F_aapcs.pdf))有很深入的了解。
第3期里还有部分同学询问Sample中的函数符号是如何来的。
首先如果我们要hook一个函数需要知道这个函数的地址。在Linux下我们获取函数的地址可以通过[dlsym](http://linux.die.net/man/3/dlsym)函数来根据名字获取动态库里的函数名称一般都会通过Name Mangling技术来生成一个符号名称具体细节可以看这篇[文章](http://www.int0x80.gr/papers/name_mangling.pdf)所以第3期的Sample里出现了很多经过转换的函数名。
```
void *hookRecordAllocation26 = ndk_dlsym(handle,
&quot;_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj&quot;);
void *hookRecordAllocation24 = ndk_dlsym(handle, &quot;_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj&quot;);
```
这样的函数可以通过[c++filt](http://linux.die.net/man/1/c++filt)工具来进行反解,我在这里给你提供一个[网页版的解析工具](http://demangler.com/)。
<img src="https://static001.geekbang.org/resource/image/12/7a/12b8af0f6363fb06f8ee10d809602c7a.png" alt="">
我们需要阅读系统源码来寻找Hook点比如第3期里Hook的方法都是虚拟机内存分配相关的函数。需要注意的一点是要先确认是否存在该函数的符号很多时候由于强制Inline的函数或者过于短小的函数可能没有对应的符号这时候就需要使用objdump、readelf、nm或者各种disassembly工具进行查看根据类名、函数名查找一下有没有对应的符号。
## 总结
第1期的Breakpad的Sample主要是展示Native Crash的日志如何获取和解读。根据业务的不同我们平时接触的很多都是Java的异常在业务不断稳定、代码异常处理逐渐完善的情况下Java异常的量会逐渐减少而Native Crash的问题会逐步的显现出来。一般比较大型的应用都会或多或少包含一些Native库比如加密、地图、日志、Push等模块由于多方面的原因这些代码会产生一些异常我们需要了解Crash日志来排查解决又或者说绕过这些异常进而提高应用的稳定性。
通过Breakpad的源码以帮你了解到信号捕获、ptrace的使用、进程fork/clone机制、主进程子进程通信、unwind stack、system info的获取、memory maps info的获取、symbol的dump以及symbol反解等通过源码我们可以学习到很多东西。
第2期的Sample提供了解决系统异常的一种思路使用反射或者代理机制来解决系统代码中的异常。需要说明的是FinalizerWatchdog机制并不是系统异常而是系统的一种防护机制。很多时候我们会遇到一些系统Framework的bug产生的Crash比如很常见的Toast异常等这些异常虽然不属于本应用产生的但也会影响用户的使用解决这种异常可以考虑一下这个Sample中的思路。
第3期的Sample描述了一个简单的Memory Allocation Trace监控模块这个模块主要是配合自动性能分析体系来自动发现问题比如大对象的分配数量监控、分配对象的调用栈分析等。它可以做的事很多同学们可以根据这个思路根据自己的业务来开发适合自己的工具。
从第3期的Sample的代码你可以学习到Inline Hook Substrate框架的使用使用ndk_dlopen来绕过Android Classloader-Namespace Restriction机制以及C++里的线程同步等。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@@ -0,0 +1,114 @@
<audio id="audio" title="练习Sample跑起来 | 热点问题答疑第2期" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/ef/4ee7f58db0aeed08c3bc148e8a6fc7ef.mp3"></audio>
你好,我是孙鹏飞。今天我们基于[专栏第5期](http://time.geekbang.org/column/article/71982)的练习Sample以及热点问题我来给你做答疑。有关上一期答疑你可以点击[这里](http://time.geekbang.org/column/article/73068)查看。
为了让同学们可以进行更多的实践专栏第5期Sample采用了让你自己实现部分功能的形式希望可以让你把专栏里讲的原理可以真正用起来。
前面几期已经有同学通过Pull request提交了练习作业这里要给每位参与练习、提交作业的同学点个赞。
第5期的作业是根据系统源码来完成一个CPU数据的采集工具并且在结尾我们提供了一个案例让你进行分析。我已经将例子的实现提交到了[GitHub](http://github.com/AndroidAdvanceWithGeektime/Chapter05)上,你可以参考一下。
在文中提到“当发生ANR的时候Android系统会打印CPU相关的信息到日志中使用的是[ProcessCpuTracker.java](http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/com/android/internal/os/ProcessCpuTracker.java)”。ProcessCpuTracker的实现主要依赖于Linux里的/proc伪文件系统in-memory pseudo-file system主要使用到了/proc/stat、/proc/loadavg、/proc/[pid]/stat、/proc/[pid]/task相关的文件来读取数据。在Linux中有很多程序都依赖/proc下的数据比如top、netstat、ifconfig等Android里常用的procrank、librank、procmem等也都以此作为数据来源。关于/proc目录的结构在Linux Man Pages里有很详细的说明在《Linux/Unix系统编程手册》这本书里也有相关的中文说明。
关于proc有一些需要说明的地方在不同的Linux内核中该目录下的内容可能会有所不同所以如果要使用该目录下的数据可能需要做一些版本上的兼容处理。并且由于Linux内核更新速度较快文档的更新可能还没有跟上这就会导致一些数据和文档中说明的不一致尤其是大量的以空格隔开的数字数据。这些文件其实并不是真正的文件你用ls查看会发现它们的大小都是0这些文件都是系统虚拟出来的读取这些文件并不会涉及文件系统的一系列操作只有很小的性能开销而现阶段并没有类似文件系统监听文件修改的回调所以需要采用轮询的方式来进行数据采集。
下面我们来看一下专栏文章结尾的案例分析。下面是这个示例的日志数据,我会通过分析数据来猜测一下是什么原因引起,并用代码还原这个情景。
```
usage: CPU usage 5000ms(from 23:23:33.000 to 23:23:38.000):
System TOTAL: 2.1% user + 16% kernel + 9.2% iowait + 0.2% irq + 0.1% softirq + 72% idle
CPU Core: 8
Load Average: 8.74 / 7.74 / 7.36
Process:com.sample.app
50% 23468/com.sample.app(S): 11% user + 38% kernel faults:4965
Threads:
43% 23493/singleThread(R): 6.5% user + 36% kernel faults3094
3.2% 23485/RenderThread(S): 2.1% user + 1% kernel faults329
0.3% 23468/.sample.app(S): 0.3% user + 0% kernel faults6
0.3% 23479/HeapTaskDaemon(S): 0.3% user + 0% kernel faults982
\.\.\.
```
上面的示例展示了一段在5秒时间内CPU的usage的情况。初看这个日志你可以收集到几个重要信息。
1.在System Total部分user占用不多CPU idle很高消耗多在kernel和iowait。
2.CPU是8核的Load Average大约也是8表示CPU并不处于高负载情况。
3.在Process里展示了这段时间内sample app的CPU使用情况user低kernel高并且有4965次page faults。
4.在Threads里展示了每个线程的usage情况当前只有singleThread处于R状态并且当前线程产生了3096次page faults其他的线程包括主线程Sample日志里可见的都是处于S状态。
根据内核中的线程状态的[宏的名字](http://elixir.bootlin.com/linux/v4.8/source/include/linux/sched.h#L207)和缩写的对应R值代表线程处于Running或者Runnable状态。Running状态说明线程当前被某个Core执行Runnable状态说明线程当前正在处于等待队列中等待某个Core空闲下来去执行。从内核里看两个状态没有区别线程都会持续执行。日志中的其他线程都处于S状态S状态代表[TASK_INTERRUPTIBLE](http://elixir.bootlin.com/linux/v4.8/ident/TASK_INTERRUPTIBLE)发生这种状态是线程主动让出了CPU如果线程调用了sleep或者其他情况导致了自愿式的上下文切换Voluntary Context Switches就会处于S状态。常见的发生S状态的原因可能是要等待一个相对较长时间的I/O操作或者一个IPC操作如果一个I/O要获取的数据不在Buffer Cache或者Page Cache里就需要从更慢的存储设备上读取此时系统会把线程挂起并放入一个等待I/O完成的队列里面在I/O操作完成后产生中断线程重新回到调度序列中。但只根据文中这个日志并不能判定是何原因所引起的。
还有就是SingleThread的各项指标都相对处于一个很高的情况而且产生了一些faults。page faluts分为三种minor page fault、major page fault和invalid page fault下面我们来具体分析。
minor page fault是内核在分配内存的时候采用一种Lazy的方式申请内存的时候并不进行物理内存的分配直到内存页被使用或者写入数据的时候内核会收到一个MMU抛出的page fault此时内核才进行物理内存分配操作MMU会将虚拟地址和物理地址进行映射这种情况产生的page fault就是minor page fault。
major page fault产生的原因是访问的内存不在虚拟地址空间也不在物理内存中需要从慢速设备载入或者从Swap分区读取到物理内存中。需要注意的是如果系统不支持[zRAM](http://source.android.com/devices/tech/perf/low-ram)来充当Swap分区可以默认Android是没有Swap分区的因为在Android里不会因为读取Swap而发生major page fault的情况。另一种情况是mmap一个文件后虚拟内存区域、文件磁盘地址和物理内存做一个映射在通过地址访问文件数据的时候发现内存中并没有文件数据进而产生了major page fault的错误。
根据page fault发生的场景虚拟页面可能有四种状态
- 第一种,未分配;
- 第二种,已经分配但是未映射到物理内存;
- 第三种,已经分配并且已经映射到物理内存;
- 第四种已经分配并映射到Swap分区在Android中此种情况基本不存在
通过上面的讲解并结合page fault数据你可以看到SingleThread你一共发生了3094次fault根据每个页大小为4KB可以知道在这个过程中SingleThread总共分配了大概12MB的空间。
下面我们来分析iowait数据。既然有iowait的占比就说明在5秒内肯定进行了I/O操作并且iowait占比还是比较大的说明当时可能进行了大量的I/O操作或者当时由于其他原因导致I/O操作缓慢。
从上面的分析可以猜测一下具体实现,并且在读和写的时候都有可能发生。由于我的手机写的性能要低一些,比较容易复现,所以下面的代码基于写操作实现。
```
File f = new File(getFilesDir(), &quot;aee.txt&quot;);
FileOutputStream fos = new FileOutputStream(f);
byte[] data = new byte[1024 * 4 * 3000];//此处分配一个12mb 大小的 byte 数组
for (int i = 0; i &lt; 30; i++) {//由于 IO cache 机制的原因所以此处写入多次cache触发 dirty writeback 到磁盘中
Arrays.fill(data, (byte) i);//当执行到此处的时候产生 minor fault并且产生 User cpu useage
fos.write(data);
}
fos.flush();
fos.close();
```
上面的代码抓取到的CPU数据如下。
```
E/ProcessCpuTracker: CPU usage from 5187ms to 121ms ago (2018-12-28 08:28:27.186 to 2018-12-28 08:28:32.252):
40% 24155/com.sample.processtracker(R): 14% user + 26% kernel / faults: 5286 minor
thread stats:
35% 24184/SingleThread(S): 11% user + 24% kernel / faults: 3055 minor
2.1% 24174/RenderThread(S): 1.3% user + 0.7% kernel / faults: 384 minor
1.5% 24155/.processtracker(R): 1.1% user + 0.3% kernel / faults: 95 minor
0.1% 24166/HeapTaskDaemon(S): 0.1% user + 0% kernel / faults: 1070 minor
100% TOTAL(): 3.8% user + 7.8% kernel + 11% iowait + 0.1% irq + 0% softirq + 76% idle
Load: 6.31 / 6.52 / 6.66
```
可以对比Sample中给出的数据基本一致。
通过上面的说明你可以如法炮制去分析ANR日志中相关的数据来查找性能瓶颈比如如果产生大量的major page fault其实是不太正常的或者iowait过高就需要关注是否有很密集的I/O操作。
## 相关资料
- [低内存配置](http://source.android.com/devices/tech/perf/low-ram)
- [iowait的形成原因和内核分析](http://oenhan.com/iowait-wa-vmstat)
- [page fault带来的性能问题](http://yq.aliyun.com/articles/55820)
- [Linux工具快速教程](http://linuxtools-rst.readthedocs.io/zh_CN/latest/index.html)
- [Android: memory management insights, part I](http://fixbugfix.blogspot.com/2015/11/android-memory-management-insights-part.html)
- [Linux 2.6调度系统分析](http://www.ibm.com/developerworks/cn/linux/kernel/l-kn26sch/index.html?mhq=iowait&amp;mhsrc=ibmsearch_a)
- 《性能之巅》
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@@ -0,0 +1,98 @@
<audio id="audio" title="练习Sample跑起来 | 热点问题答疑第3期" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/67/fb/6743895ed4b5615995b787a754d154fb.mp3"></audio>
你好我是孙鹏飞。又到了答疑的时间今天我将围绕卡顿优化这个主题和你探讨一下专栏第6期和补充篇的两个Sample的实现。
专栏第6期的Sample完全来自于Facebook的性能分析框架[Profilo](https://github.com/facebookincubator/profilo)主要功能是收集线上用户的atrace日志。关于atrace相信我们都比较熟悉了平时经常使用的systrace工具就是封装了atrace命令来开启ftrace事件并读取ftrace缓冲区生成可视化的HTML日志。这里多说一句ftrace是Linux下常用的内核跟踪调试工具如果你不熟悉的话可以返回第6期文稿最后查看ftrace的介绍。Android下的atrace扩展了一些自己使用的categories和tag这个Sample获取的就是通过atrace的同步事件。
Sample的实现思路其实也很简单有两种方案。
第一种方案hook掉atrace写日志时的一系列方法。以Android 9.0的代码为例写入ftrace日志的代码在[trace-dev.cpp](http://androidxref.com/9.0.0_r3/xref/system/core/libcutils/trace-dev.cpp)里,由于每个版本的代码有些区别,所以需要根据系统版本做一些区分。
第二种方案也是Sample里所使用的方案由于所有的atrace event写入都是通过[/sys/kernel/debug/tracing/trace_marker](http://androidxref.com/9.0.0_r3/xref/system/core/libcutils/trace-container.cpp#85)atrace在初始化的时候会将该路径fd的值写入[atrace_marker_fd](http://androidxref.com/9.0.0_r3/s?defs=atrace_marker_fd&amp;project=system)全局变量中我们可以通过dlsym轻易获取到这个fd的值。关于trace_maker这个文件我需要说明一下这个文件涉及ftrace的一些内容ftrace原来是内核的事件trace工具并且ftrace文档的开头已经写道
>
Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on inside the kernel.
从文档中可以看出来ftrace工具主要是用来探查outside of user-space的性能问题。不过在很多场景下我们需要知道user space的事件调用和kernel事件的一个先后关系所以ftrace也提供了一个解决方法也就是提供了一个文件trace_marker往该文件中写入内容可以产生一条ftrace记录这样我们的事件就可以和kernel的日志拼在一起。但是这样的设计有一个不好的地方在往文件写入内容的时候会发生system call调用有系统调用就会产生用户态到内核态的切换。这种方式虽然没有内核直接写入那么高效但在很多时候ftrace工具还是很有用处的。
由此可知用户态的事件数据都是通过trace_marker写入的更进一步说是通过write接口写入的那么我们只需要hook住write接口并过滤出写入这个fd下的内容就可以了。这个方案通用性比较高而且使用PLT Hook即可完成。
下一步会遇到的问题是想要获取atrace的日志就需要设置好atrace的category tag才能获取到。我们从源码中可以得知判断tag是否开启是通过atrace_enabled_tags &amp; tag来计算的如果大于0则认为开启等于0则认为关闭。下面我贴出了部分atrace_tag的值你可以看到判定一个tag是否是开启的只需要tag值的左偏移数的位值和atrace_enabled_tags在相同偏移数的位值是否同为1。其实也就是说我将atrace_enabled_tags的所有位都设置为1那么在计算时候就能匹配到任何的atrace tag。
```
#define ATRACE_TAG_NEVER 0
#define ATRACE_TAG_ALWAYS (1&lt;&lt;0)
#define ATRACE_TAG_GRAPHICS (1&lt;&lt;1)
#define ATRACE_TAG_INPUT (1&lt;&lt;2)
#define ATRACE_TAG_VIEW (1&lt;&lt;3)
#define ATRACE_TAG_WEBVIEW (1&lt;&lt;4)
#define ATRACE_TAG_WINDOW_MANAGER (1&lt;&lt;5)
#define ATRACE_TAG_ACTIVITY_MANAGER (1&lt;&lt;6)
#define ATRACE_TAG_SYNC_MANAGER (1&lt;&lt;7)
#define ATRACE_TAG_AUDIO (1&lt;&lt;8)
#define ATRACE_TAG_VIDEO (1&lt;&lt;9)
#define ATRACE_TAG_CAMERA (1&lt;&lt;10)
#define ATRACE_TAG_HAL (1&lt;&lt;11)
#define ATRACE_TAG_APP (1&lt;&lt;12)
```
下面是我用atrace抓下来的部分日志。
<img src="https://static001.geekbang.org/resource/image/f9/b8/f9b273a45eeb643f976b48147ce1b3b8.png" alt="">
看到这里有同学会问Begin和End是如何对应上的呢要回答这个问题首先要先了解一下这种记录产生的场景。这个日志在Java端是由Trace.traceBegin和Trace.traceEnd产生的在使用上有一些硬性要求这两个方法必须成对出现否则就会造成日志的异常。请看下面的系统代码示例。
```
void assignWindowLayers(boolean setLayoutNeeded) {
2401 Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, &quot;assignWindowLayers&quot;);//关注此处事件开始代码
2402 assignChildLayers(getPendingTransaction());
2403 if (setLayoutNeeded) {
2404 setLayoutNeeded();
2405 }
2406
2411 scheduleAnimation();
2412 Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);//事件结束
2413 }
2414
```
所以我们可以认为B下面紧跟的E就是事件的结束标志但很多情况下我们会遇到上面日志中所看到的两个B连在一起紧跟的两个E我们不知道分别对应哪个B。此时我们需要看一下产生事件的CPU是哪个并且看一下产生事件的task_pid是哪个也就是最前面的InputDispatcher-1944这样我们就可以对应出来了。
接下来我们一起来看看补充篇的Sample它的目的是希望让你练习一下如何监控线程创建并且打印出创建线程的Java方法。Sample的实现比较简单主要还是依赖PLT Hook来hook线程创建时使用的主要函数pthread_create。想要完成这个Sample你需要知道Java线程是如何创建出来的并且还要理解Java线程的执行方式。需要特别说明的是其实这个Sample也存在一个缺陷。从虚拟机的角度看线程其实又分为两种一种是Attached线程我习惯按照.Net的叫法称其为托管线程一种是Unattached线程为非托管线程。但底层都是依赖POSIX Thread来实现的从pthread_create里无法区分该线程是否是托管线程也有可能是Native直接开启的线程所以有可能并不能对应到创建线程时候的Java Stack。
关于线程我们在日常监控中可能并不太关心线程创建时候的状况而区分线程可以通过提前设置Thread Name来实现。举个例子比如在出现OOM时发现是发生在pthread_create执行的时候说明当前线程数可能过多一般我们会在OOM的时候采集当前线程数和线程堆栈信息可以看一下是哪个线程创建过多如果指定了线程名称则很快就能查找出问题所在。
对于移动端的线程来说我们大多时候更关心的是主线程的执行状态。因为主线程的任何耗时操作都会影响操作界面的流畅度所以我们经常把看起来比较耗时的操作统统都往子线程里面丢虽然这种操作虽然有时候可能很有效但还可能会产生一些我们平时很少遇到的异常情况。比如我曾经遇到过由于用户手机的I/O性能很低大量的线程都在wait io或者线程开启的太多导致线程Context switch过高又或者是一个方法执行过慢导致持有锁的时间过长其他线程无法获取到锁等一系列异常的情况
虽然线程的监控很不容易但并不是不能实现只是实现起来比较复杂并且要考虑兼容性。比如我们可能比较关心一个Lock当前有多少线程在等待锁释放就需要先获取到这个Object的MirrorObject然后构造一个MonitorInfo之后获取到waiters的列表而这个列表里就存储了等待锁释放的线程。你看其实过程也并不复杂只是在计算地址偏移量的时候需要做一些处理。
当然还有更细致的优化比如我们都知道Java里是有轻量级锁和重量级锁的一个转换过程在ART虚拟机里被称为ThinLocked和FatLocked而转换过程是通过Monitor::Inflate和Monitor::Deflate函数来实现的。此时我们可以监控Monitor::Inflate调用时monitor指向的Object来判断是哪段代码产生了“瘦锁”到“胖锁”转换的过程从而去做一些优化。接下来要做优化需要先知晓ART虚拟机锁转换的机制如果当前锁是瘦锁持有该锁的线程再一次获取这个锁只递增了lock count并未改变锁的状态。但是lock count超过4096则会产生瘦锁到胖锁的转换如果当前持有该锁的线程和进入MontorEnter的线程不是同一个的情况下就会产生锁争用的情况。ART虚拟机为了减少胖锁的产生做了一些优化虚拟机先通过[sched_yield](http://man7.org/linux/man-pages/man2/sched_yield.2.html)让出当前线程的执行权操作系统在后面的某个时间再次调度该线程执行从调用sched_yield到再次执行的时候计算时间差在这个时间差里占用该锁的线程可能会释放对锁的占用那么调用线程会再次尝试获取锁如果获取锁成功的话则会从 Unlocked状态直接转换为ThinLocked状态不会产生FatLocked状态。这个过程持续50次如果在50次循环内无法获取到锁则会将瘦锁转为胖锁。如果我们对某部分的多线程代码性能敏感则希望锁尽量持续在瘦锁的状态我们可以减少同步块代码的粒度尽量减少很多线程同时争抢锁可以监控Inflate函数调用情况来判断优化效果。
最后还有同学对在Crash状态下获取Java线程堆栈的方法比较感兴趣我在这里简单讲一下后面会有专门的文章介绍这部分内容。
一种方案是使用ThreadList::ForEach接口间接实现具体的逻辑可以看[这里](http://androidxref.com/9.0.0_r3/xref/art/runtime/trace.cc#286)。另一种方案是 Profilo里的[Unwinder](https://github.com/facebookincubator/profilo/blob/master/cpp/profiler/unwindc/)机制,这种实现方式就是模拟[StackVisitor](http://androidxref.com/9.0.0_r3/xref/art/runtime/stack.cc#766)的逻辑来实现。
这两期反馈的问题不多答疑的内容也可以算作对正文的补充如果有同学想多了解虚拟机的机制或者其他性能相关的问题欢迎你给我留言我也会在后面的文章和你聊聊这些话题比如有同学问到的ART下GC的详细逻辑之类的问题。
## 相关资料
<li>
[ftrace kernel doc](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/trace/ftrace.rst)
</li>
<li>
[ftrace的使用](https://source.android.google.cn/devices/tech/debug/ftrace)
</li>
<li>
[A look at ftrace](https://lwn.net/Articles/322666/)
</li>
## 福利彩蛋
今天为认真提交作业完成练习的同学,送出第二波“学习加油礼包”。@Seven同学提交了第5期的[作业](https://github.com/AndroidAdvanceWithGeektime/Chapter05/pull/1)送出“极客周历”一本其他同学如果完成了练习千万别忘了通过Pull request提交哦。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@@ -0,0 +1,39 @@
<audio id="audio" title="练习Sample跑起来 | 热点问题答疑第4期" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4a/04/4a0b0d36eab51f49f932bb3a72102704.mp3"></audio>
你好我是孙鹏飞。今天我们回到专栏第7期和第8期来看看课后练习Sample的运行需要注意哪些问题。另外我结合同学们留言的疑问也来谈谈文件顺序对I/O的影响以及关于Linux学习我的一些方法和建议。
[专栏第7期](http://time.geekbang.org/column/article/73651)的Sample借助于systrace工具通过字节码处理框架对函数插桩来获取方法执行的trace。这个Sample实现相当完整你在日常工作也可以使用它。
这个Sample使用起来虽然非常简单但其内部的实现相对来说是比较复杂的。它的实现涉及Gradle Transform、Task实现、增量处理、ASM字节码处理、mapping文件使用以及systrace工具的使用等。
对于Gradle来说我们应该比较熟悉它是Android平台下的构建工具。对于平时使用来说我们大多时候只需要关注Android Gradle Plugin的一些参数配置就可以实现很多功能了官方文档已经提供了很详细的参数设置[说明](https://developer.android.com/studio/build/?hl=zh-cn)。对于一些需要侵入打包流程的操作就需要我们实现自己的Task或者Transform代码来完成比如处理Class和JAR包、对资源做一些处理等。
Gradle学习的困难更多来自于Android Gradle Plugin对Gradle做的一些封装扩展而这部分Google并没有提供很完善的文档并且每个版本都有一些接口上的变动。对于这部分内容的学习我主要是去阅读别人实现的Gradle工具代码和[Android Gradle Plugin代码](https://android.googlesource.com/platform/tools/base/+/studio-3.2.1/build-system/)。
关于这期的Sample实现有几个可能产生疑问的地方我们来探讨一下。
这个Sample的Gradle插件是发布到本地Maven库的所以如果没有执行发布直接编译需要先发布插件库到本地Maven中才能执行编译成功。
另一个可能遇到问题的是如果你想把Sample使用到其他项目需要自己将SampleApp中其p的e.systrace.TraceTag”类移植到自己的项目中否则会产生编译错误。
对于字节码处理在Sample中主要使用了ASM框架来处理。市面上关于字节码处理的框架有很多常见的有[ASM和Javassist框架](https://www.infoq.cn/article/Living-Matrix-Bytecode-Manipulation)其他的框架你可以使用“Java bytecode manipulation”关键字在Google上搜索。使用字节码处理框架需要对字节码有比较深入的了解要提醒你的是这里的字节码不是Dalvik bytecode而是Java bytecode。对于字节码的学习你可以参考[官方文档](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html)和《Java虚拟机规范》里面对字节码的执行规则和指令说明都有很详细的描述。并且还可以配合javap命令查看反编译的字节码对应的源码这样学习下来会有很好的效果。字节码处理是一个很细微的操作稍有失误就会产生编译错误、执行错误或者Crash的情况里面需要注意的地方也非常多比如Try Catch Block对操作数栈的影响、插入的代码对本地变量表和操作数栈的影响等。
实现AOP的另一种方是可以接操作Dex文件进行Dalvik bytecode字节码注入关于这种实现方式可以使用[dexer](https://android.googlesource.com/platform/tools/dexter/)库来完成在Facebook的[Redex](https://github.com/facebook/redex)中也提供了针对dex的AOP功能。
下面我们来看[专栏第8期](http://time.geekbang.org/column/article/74044)。我从文章留言里看到有同学关于数据重排序对I/O性能的影响有些疑问不太清楚优化的原理。其实这个优化原理理解起来是很容易的在进行文件读取的操作过程中系统会读取比预期更多的文件内容并缓存在Page Cache中这样下一次读请求到来时部分页面直接从Page Cache读取而不用再从磁盘中获取数据这样就加速了读取的操作。在[《支付宝App构建优化解析》](https://mp.weixin.qq.com/s/79tAFx6zi3JRG-ewoapIVQ)里“原理”一节中已经有比较详细的描述,我就不多赘述了。如果你对“预读”感兴趣的话,我给你提供一些资料,可以深入了解一下。
预读readhead机制的系统源码在[readhead.c](https://github.com/torvalds/linux/blob/master/mm/readahead.c)文件中。需要说明的是,预读机制可能在不同系统版本中有所变化,所以下面我提供的资料大多是基于 Linux 2.6.x的内核在这以后的系统版本可能对 readhead 机制有修改,你需要留意一下。
关于预读机制详细的算法说明可以看[《Linux readahead: less tricks for more》](https://www.kernel.org/doc/ols/2007/ols2007v2-pages-273-284.pdf)和[《Sequential File Prefetching In Linux》](http://www.ece.eng.wayne.edu/~sjiang/Tsinghua-2010/linux-readahead.pdf)、[《Linux内核的文件预读readahead](http://blog.51cto.com/wangergui/1841294) 这三篇文档。
从专栏前几篇的正文看很多优化的内容是从Linux的机制入手的如果你对Linux的机制和优化不了解的话是不太容易想到这些方案的。举个例子专栏文章提到的小文件系统是运行在用户态的代码底层依然依赖现存文件系统提供的功能因此需要深入了解Linux VFS、ext4的实现以及它们的优缺点和原理这样我们才能发现为什么大量的小文件依赖现存的文件系统管理是存在性能缺陷的以及下一步如何填补这些性能缺陷。
作为Android开发工程师我们该何学习Linux呢我其实不建议上来就直接阅读系统源码分析相关的书我建议是从理解操作系统概念开始推荐两本操作系统相关的书《深入理解计算机系统》和《计算机系统 系统架构与操作系统的高度集成》。Linux的系统实现其实和传统的操作系统概念在细节上会有不小的差别再推荐一本解析Linux操作系统的书《操作系统之编程观察》这本书结合源码对Linux的各方面机制都进行和很详细的分析。
对于从事Android开发的同学来说确实很有必要深入了解Linux系统相关的知识因为Android里很多特性都是依赖底层基础系统的就比如我刚刚提到的“预读”机制不光可以用在Android的资源加载上也可以拓展到Flutter的资源加载上。假如我们以后面对一个不是Linux内核的系统比如Fuchsia OS也可以根据已经掌握的系统知识套用到现有的操作系统上因为像内存管理、文件系统、信号机制、进程调度、系统调用、中断机制、驱动等内容都是共通的在迁移到新的系统上的时候可以有一个全局的学习视角帮助我们快速上手。对于操作系统内容我的学习路线是先熟悉系统机制然后熟悉系统提供的各个方向的接口比如I/O操作、进程创建、信号中断处理、线程使用、epoll、通信机制等按照《UNIX环境高级编程》这本书的内容一步步的走就可以完成这一步骤熟悉之后可以按照自己的节奏再去学习自己比较感兴趣的模块。此时可以找一本源码分析的书再去阅读比如想了解fork机制的实现、I/O操作的read和write在内核态的调度执行像这些问题就需要有目的性的进行挖掘。
上面这个学习路线是我在学习过程中不断踩坑总结出来的一些经验,对于操作系统我也只是个初学者,也欢迎你留言说说自己学习的经验和问题,一起切磋进步。
最后送出3本“极客周历”给用户故事“[专栏学得苦?可能是方法没找对](http://time.geekbang.org/column/article/77342)”留言点赞数前三的同学,分别是@坚持远方@蜗牛@JIA,感谢同学们的参与。