mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-10 19:54:28 +08:00
mod
This commit is contained in:
189
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | ASM插桩强化练习.md
Normal file
189
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | ASM插桩强化练习.md
Normal 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, "java/lang/System", "currentTimeMillis", "()J", false);
|
||||
timeLocalIndex = newLocal(Type.LONG_TYPE); //这个是LocalVariablesSorter 提供的功能,可以尽量复用以前的局部变量
|
||||
mv.visitVarInsn(LSTORE, timeLocalIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMethodExit(int opcode) {
|
||||
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
|
||||
mv.visitVarInsn(LLOAD, timeLocalIndex);
|
||||
mv.visitInsn(LSUB);//此处的值在栈顶
|
||||
mv.visitVarInsn(LSTORE, timeLocalIndex);//因为后面要用到这个值所以先将其保存到本地变量表中
|
||||
int stringBuilderIndex = newLocal(Type.getType("java/lang/StringBuilder"));
|
||||
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
|
||||
mv.visitInsn(Opcodes.DUP);
|
||||
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
|
||||
mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);//需要将栈顶的 stringbuilder 保存起来否则后面找不到了
|
||||
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
|
||||
mv.visitLdcInsn(className + "." + methodName + " time:");
|
||||
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
|
||||
mv.visitInsn(Opcodes.POP);//将 append 方法的返回值从栈里 pop 出去
|
||||
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
|
||||
mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
|
||||
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
|
||||
mv.visitInsn(Opcodes.POP);//将 append 方法的返回值从栈里 pop 出去
|
||||
mv.visitLdcInsn("Geek");
|
||||
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
|
||||
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
|
||||
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", 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 && "java/lang/Thread".equals(s)) {
|
||||
findNew = true;//遇到new指令
|
||||
mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");//替换new指令的类名
|
||||
return;
|
||||
}
|
||||
super.visitTypeInsn(opcode, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
|
||||
//需要排查CustomThread自己
|
||||
if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && findNew) {
|
||||
findNew= false;
|
||||
mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", 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.<init> (I)V
|
||||
INVOKESPECIAL A.<init> (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 ~ 8,12,17,19)](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&utm_medium=geektime&utm_campaign=yuedu&utm_term=0223)
|
||||
|
||||
|
||||
416
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 唯鹿同学的练习手记 第1辑.md
Normal file
416
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 唯鹿同学的练习手记 第1辑.md
Normal 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.9,libcrash-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 < 1000; i++) {
|
||||
Message msg = new Message();
|
||||
msg.what = i;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
因为代码从点击开始记录开始,触发到5000的数据就dump到文件中,点击5次后就会在`sdcard/crashDump`下生成一个时间戳命名的文件。项目根目录下调用命令:
|
||||
|
||||
```
|
||||
java -jar tools/DumpPrinter-1.0.jar dump文件路径 > 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.<init> (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("start hookFunc");
|
||||
void *handle = ndk_dlopen("libart.so", RTLD_LAZY | RTLD_GLOBAL);
|
||||
|
||||
if (!handle) {
|
||||
LOGE("libart.so open fail");
|
||||
return;
|
||||
}
|
||||
void *hookRecordAllocation26 = ndk_dlsym(handle,
|
||||
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj");
|
||||
|
||||
void *hookRecordAllocation24 = ndk_dlsym(handle,
|
||||
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj");
|
||||
|
||||
void *hookRecordAllocation23 = ndk_dlsym(handle,
|
||||
"_ZN3art3Dbg16RecordAllocationEPNS_6ThreadEPNS_6mirror5ClassEj");
|
||||
|
||||
void *hookRecordAllocation22 = ndk_dlsym(handle,
|
||||
"_ZN3art3Dbg16RecordAllocationEPNS_6mirror5ClassEj");
|
||||
|
||||
if (hookRecordAllocation26 != nullptr) {
|
||||
LOGI("Finish get symbol26");
|
||||
MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26,
|
||||
(void **) &oldArtRecordAllocation26);
|
||||
|
||||
} else if (hookRecordAllocation24 != nullptr) {
|
||||
LOGI("Finish get symbol24");
|
||||
MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26,
|
||||
(void **) &oldArtRecordAllocation26);
|
||||
|
||||
} else if (hookRecordAllocation23 != NULL) {
|
||||
LOGI("Finish get symbol23");
|
||||
MSHookFunction(hookRecordAllocation23, (void *) &newArtRecordAllocation23,
|
||||
(void **) &oldArtRecordAllocation23);
|
||||
} else {
|
||||
LOGI("Finish get symbol22");
|
||||
if (hookRecordAllocation22 == NULL) {
|
||||
LOGI("error find hookRecordAllocation22");
|
||||
return;
|
||||
} else {
|
||||
MSHookFunction(hookRecordAllocation22, (void *) &newArtRecordAllocation22,
|
||||
(void **) &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, &a);
|
||||
//达到 max
|
||||
if (allocObjectCount > setAllocRecordMax) {
|
||||
CMyLock lock(g_Lock);//此处需要 loc 因为对象分配的时候不知道在哪个线程,不 lock 会导致重复 dump
|
||||
allocObjectCount = 0;
|
||||
|
||||
// dump alloc 里的对象转换成 byte 数据
|
||||
jbyteArray allocData = getARTAllocationData();
|
||||
// 将alloc数据写入文件
|
||||
SaveAllocationData saveData{allocData};
|
||||
saveARTAllocationData(saveData);
|
||||
resetARTAllocRecord();
|
||||
LOGI("===========CLEAR ALLOC MAPS=============");
|
||||
|
||||
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("android.graphics.Bitmap");
|
||||
// 获得heap, 只需要分析app和default heap即可
|
||||
Collection<Heap> heaps = snapshot.getHeaps();
|
||||
|
||||
for (Heap heap : heaps) {
|
||||
// 只需要分析app和default heap即可
|
||||
if (!heap.getName().equals("app") && !heap.getName().equals("default")) {
|
||||
continue;
|
||||
}
|
||||
for (ClassObj clazz : bitmapClasses) {
|
||||
//从heap中获得所有的Bitmap实例
|
||||
List<Instance> bitmapInstances = clazz.getHeapInstances(heap.getId());
|
||||
//从Bitmap实例中获得buffer数组,宽高信息等。
|
||||
ArrayInstance buffer = HahaHelper.fieldValue(((ClassInstance) bitmapInstance).getValues(), "mBuffer");
|
||||
int bitmapHeight = fieldValue(bitmapInstance, "mHeight");
|
||||
int bitmapWidth = fieldValue(bitmapInstance, "mWidth");
|
||||
// 引用链信息
|
||||
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 faults:3094
|
||||
3.2% 23485/RenderThread(S): 2.1% user + 1% kernel faults:329
|
||||
0.3% 23468/.sample.app(S): 0.3% user + 0% kernel faults:6
|
||||
0.3% 23479/HeapTaskDaemon(S): 0.3% user + 0% kernel faults:982
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
因为了解Linux不多,所以看这个有点懵逼。好在课代表孙鹏飞同学解答了相关问题,看懂了上面信息,同时学习到了一些Linux知识。
|
||||
|
||||
```
|
||||
private void testIO() {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
File f = new File(getFilesDir(), "aee.txt");
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
byte[] data = new byte[1024 * 4 * 3000];// 此处分配一个 12mb 大小的 byte 数组
|
||||
|
||||
for (int i = 0; i < 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("SingleThread");
|
||||
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>
|
||||
|
||||
|
||||
318
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 唯鹿同学的练习手记 第2辑.md
Normal file
318
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 唯鹿同学的练习手记 第2辑.md
Normal 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),今天练习6~8、12、17、19这六期内容(主要针对有课后Sample练习的),相比1~5期轻松了很多。
|
||||
|
||||
[**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 > 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("hook_plt_method");
|
||||
hook_plt_method_all_lib("libc.so", "send", (hook_func) &socket_send_hook);
|
||||
hook_plt_method_all_lib("libc.so", "recv", (hook_func) &socket_recv_hook);
|
||||
hook_plt_method_all_lib("libc.so", "sendto", (hook_func) &socket_sendto_hook);
|
||||
hook_plt_method_all_lib("libc.so", "recvfrom", (hook_func) &socket_recvfrom_hook);
|
||||
hook_plt_method_all_lib("libc.so", "connect", (hook_func) &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& 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===>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:<!DOCTYPE html>
|
||||
<html><!--STATUS OK--><head><meta charset="utf-8"><title>百度一下,你就知道</title>
|
||||
|
||||
```
|
||||
|
||||
可以看到我们获取到了网络请求的相关信息。
|
||||
|
||||
最后,我们可以通过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<?> clazz = oldObj.getClass();
|
||||
Field field = clazz.getDeclaredField("mService");
|
||||
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)
|
||||
|
||||
|
||||
354
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 唯鹿同学的练习手记 第3辑.md
Normal file
354
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 唯鹿同学的练习手记 第3辑.md
Normal 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 && ./configure && 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("execution(* **(..))")
|
||||
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("execution(* **(..))")
|
||||
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("execution(* com.sample.systrace.MainActivity.onResume())") // <- 指定类与方法
|
||||
public void methodTryCatch() {
|
||||
}
|
||||
|
||||
@Around("methodTryCatch()")
|
||||
public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
|
||||
// try catch
|
||||
try {
|
||||
joinPoint.proceed(); // <- 调用原方法
|
||||
} 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("com/sample/asm/MainActivity") && methodName.equals("mm")) {
|
||||
mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/lang/Exception");
|
||||
mv.visitLabel(tryStart);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMethodExit(int opcode) {
|
||||
if (className.equals("com/sample/asm/MainActivity") && methodName.equals("mm")) {
|
||||
mv.visitLabel(tryEnd);
|
||||
mv.visitJumpInsn(GOTO, catchEnd);
|
||||
mv.visitLabel(catchStart);
|
||||
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/RuntimeException", "printStackTrace", "()V", 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("android/telephony/TelephonyManager") && name.equals("getDeviceId") && desc.equals("()Ljava/lang/String;")) {
|
||||
Log.e("asmcode", "get imei className:%s, method:%s, name:%s", 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)
|
||||
|
||||
|
||||
197
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第1期.md
Normal file
197
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第1期.md
Normal 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和NDK,SDK我们一般都是配置好的,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 -> Project Structure -> 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 > Android > 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 "-std=c++11"
|
||||
arguments "-DANDROID_TOOLCHAIN=gcc"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
第二个问题是**日志解析工具如何获取**。
|
||||
|
||||
解析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平台的内核是Linux,Android里的动态链接库的符号表导出工具**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 && 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 > 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 > 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,
|
||||
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj");
|
||||
|
||||
void *hookRecordAllocation24 = ndk_dlsym(handle, "_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj");
|
||||
|
||||
```
|
||||
|
||||
这样的函数可以通过[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++里的线程同步等。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。
|
||||
|
||||
|
||||
114
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第2期.md
Normal file
114
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第2期.md
Normal 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 faults:3094
|
||||
3.2% 23485/RenderThread(S): 2.1% user + 1% kernel faults:329
|
||||
0.3% 23468/.sample.app(S): 0.3% user + 0% kernel faults:6
|
||||
0.3% 23479/HeapTaskDaemon(S): 0.3% user + 0% kernel faults:982
|
||||
\.\.\.
|
||||
|
||||
```
|
||||
|
||||
上面的示例展示了一段在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(), "aee.txt");
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
|
||||
byte[] data = new byte[1024 * 4 * 3000];//此处分配一个12mb 大小的 byte 数组
|
||||
|
||||
for (int i = 0; i < 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&mhsrc=ibmsearch_a)
|
||||
- 《性能之巅》
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。
|
||||
|
||||
|
||||
98
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第3期.md
Normal file
98
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第3期.md
Normal 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&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 & 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<<0)
|
||||
#define ATRACE_TAG_GRAPHICS (1<<1)
|
||||
#define ATRACE_TAG_INPUT (1<<2)
|
||||
#define ATRACE_TAG_VIEW (1<<3)
|
||||
#define ATRACE_TAG_WEBVIEW (1<<4)
|
||||
#define ATRACE_TAG_WINDOW_MANAGER (1<<5)
|
||||
#define ATRACE_TAG_ACTIVITY_MANAGER (1<<6)
|
||||
#define ATRACE_TAG_SYNC_MANAGER (1<<7)
|
||||
#define ATRACE_TAG_AUDIO (1<<8)
|
||||
#define ATRACE_TAG_VIDEO (1<<9)
|
||||
#define ATRACE_TAG_CAMERA (1<<10)
|
||||
#define ATRACE_TAG_HAL (1<<11)
|
||||
#define ATRACE_TAG_APP (1<<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, "assignWindowLayers");//关注此处事件开始代码
|
||||
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提交哦。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。
|
||||
|
||||
|
||||
39
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第4期.md
Normal file
39
极客时间专栏/Android开发高手课/练习Sample跑起来/练习Sample跑起来 | 热点问题答疑第4期.md
Normal 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,感谢同学们的参与。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user