-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
16892 lines (14767 loc) · 813 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>2048</title>
<url>/2017/02/18/2048/</url>
<content><![CDATA[在gayhub上发现一个挺有意思的小程序————2048
{% asset_img 2048.jpg 2048 %}
<!-- more -->
## 下载安装试玩
```bash
$ wget https://raw.githubusercontent.com/mevdschee/2048.c/master/2048.c
$ gcc 2048.c
$ ./a.out
```
## 源代码
```C
#include <iostream>
#include <windows.h>
#include <ctime>
using namespace std;
int const ROW = 4;
int const COL = 4;
int game[ROW][COL] = {0};
//上下左右
int const UP = 1;
int const DOWN = 2;
int const LEFT = 3;
int const RIGHT = 4;
//游戏所处的状态
int const GAME_OVER = 1;
int const GAME_WIN = 2;
int const GAME_CONTINUE = 3;
enum GameNum
{
Game_2 = 2,
Game_4 = 4,
Game_8 = 8,
Game_16 = 16,
Game_32 = 32,
Game_64 = 64,
Game_128 = 128,
Game_256 = 256,
Game_512 = 512,
Game_1024 = 1024,
Game_2048 = 2048,
};
//打印所得的数组
void Print()
{
system("cls");
cout << "***************** 2048 控 制 台 版 ******************" << endl;
cout << "***************** By Tanzf (Intern) ******************" << endl << endl;
for (int i = 0; i < ROW; ++i)
{
cout << "---------------------------------"<< endl;
for (int j = 0; j < COL; ++j)
{
if (game[i][j] == 0)
{
cout <<"| \t";
}
else
{
cout <<"| " << game[i][j] << "\t";
}
}
cout << "|" << endl;
}
cout << "---------------------------------"<< endl;
}
bool CreateNumber()
{
int x = -1;
int y = -1;
int times = 0;
int maxTimes = ROW * COL;
//三分之二的概率生成2,三分之一的概率生成4
int whitch = rand() % 3;
do
{
x = rand() % ROW;
y = rand() % COL;
++times;
} while (game[x][y] != 0 && times <= maxTimes);
//说明格子已经满了
if(times >= maxTimes)
{
return false;
}
else
{
GameNum num;
if(whitch == 0)
{
num = Game_4;
}
else if(whitch)
{
num = Game_2;
}
game[x][y] = num;
}
return true;
}
void Process(int direction)
{
switch (direction)
{
case UP:
//最上面一行不动
for(int row = 1; row < ROW; ++row)
{
for(int crow = row; crow >= 1; --crow)
{
for(int col = 0; col < COL; ++col)
{
//上一个格子为空
if(game[crow-1][col] == 0)
{
game[crow-1][col] = game[crow][col];
game[crow][col] = 0;
}
else
{
//合并
if(game[crow-1][col] == game[crow][col])
{
game[crow - 1][col] *= 2;
game[crow][col] = 0;
}
}
}
}
}
break;
case DOWN:
//最下面一行不动
for(int row = ROW - 2; row >= 0; --row)
{
for(int crow = row; crow < ROW - 1; ++crow)
{
for(int col = 0; col < COL; ++col)
{
//上一个格子为空
if(game[crow + 1][col] == 0)
{
game[crow + 1][col] = game[crow][col];
game[crow][col] = 0;
}
else
{
//合并
if(game[crow + 1][col] == game[crow][col])
{
game[crow + 1][col] *= 2;
game[crow][col] = 0;
}
}
}
}
}
break;
case LEFT:
//最左边一列不动
for(int col = 1; col < COL; ++col)
{
for(int ccol = col; ccol >= 1; --ccol)
{
for(int row = 0; row < ROW; ++row)
{
//上一个格子为空
if(game[row][ccol-1] == 0)
{
game[row][ccol - 1] = game[row][ccol];
game[row][ccol] = 0;
}
else
{
//合并
if(game[row][ccol - 1] == game[row][ccol])
{
game[row][ccol - 1] *= 2;
game[row][ccol] = 0;
}
}
}
}
}
break;
case RIGHT:
//最右边一列不动
for(int col = COL - 2; col >= 0; --col)
{
for(int ccol = col; ccol <= COL - 2; ++ccol)
{
for(int row = 0; row < ROW; ++row)
{
//上一个格子为空
if(game[row][ccol + 1] == 0)
{
game[row][ccol + 1] = game[row][ccol];
game[row][ccol] = 0;
}
else
{
//合并
if(game[row][ccol + 1] == game[row][ccol])
{
game[row][ccol + 1] *= 2;
game[row][ccol] = 0;
}
}
}
}
}
break;
}
}
//处理输入输出,返回上下左右
int Input()
{
//读取上下左右四个方向键
int upArrow = 0;
int downArrow = 0;
int leftArrow = 0;
int rightArrow = 0;
int direction = 0;
while (true)
{
upArrow = GetAsyncKeyState(VK_UP);
downArrow = GetAsyncKeyState(VK_DOWN);
leftArrow = GetAsyncKeyState(VK_LEFT);
rightArrow = GetAsyncKeyState(VK_RIGHT);
if(upArrow)
{
direction = UP;
break;
}
else if(downArrow)
{
direction = DOWN;
break;
}
else if(leftArrow)
{
direction = LEFT;
break;
}
else if(rightArrow)
{
direction = RIGHT;
break;
}
Sleep(100);
}
return direction;
}
//判断游戏状态
int Judge()
{
//赢得游戏
for(int i = 0; i < ROW; ++i)
{
for(int j = 0; j < COL; ++j)
{
if(game[i][j] == 2048)
{
return GAME_WIN;
break;
}
}
}
//横向检查
for(int i = 0 ; i < ROW; ++i)
{
for(int j = 0; j < COL - 1; ++j)
{
if(!game[i][j] || (game[i][j] == game[i][j+1]))
{
return GAME_CONTINUE;
break;
}
}
}
//纵向检查
for(int j = 0; j< COL; ++j)
{
for(int i = 0; i < ROW -1; ++i)
{
if(!game[i][j] || (game[i][j] == game[i+1][j]))
{
return GAME_CONTINUE;
break;
}
}
}
//不符合上述两种状况,游戏结束
return GAME_OVER;
}
int main()
{
//设置一个随机数种子
srand((unsigned int)time(0));
CreateNumber();
CreateNumber();
Print();
int direction = 0;
int gameState = -1;
while(true)
{
direction = Input();
gameState = Judge();
if(direction && gameState == GAME_CONTINUE)
{
Process(direction);
CreateNumber();
Print();
Sleep(100);
}
else if(gameState == GAME_WIN)
{
Print();
cout << "You Win!" << endl;
break;
}
else if(gameState == GAME_OVER)
{
Print();
cout <<"You lose!" << endl;
break;
}
}
return 0;
}
```
## PS
此代码仅可在Linux上使用,Windows上会提示缺失头文件,问了问度娘,虽然有不少这样的代码,但是这份代码的显示效果最好,所以就是它啦。
{% cq %}生命重在折腾{% endcq %}]]></content>
<tags>
<tag>C</tag>
<tag>C++</tag>
</tags>
</entry>
<entry>
<title>用树莓派和四位数码管模块做一个时钟</title>
<url>/2019/01/10/4-BIT-LED-Digital-Tube-Moudle/</url>
<content><![CDATA[
## 预备知识
{% asset_img 1.jpg %}
如图,这是一个4位数码管模块,由两片 74HC595 级联控制,引脚如图,从上到下,分别为`VCC(3.3V/5V)`,`SCLK(移位控制端)`,`RCLK(输出控制端)`,`DIO(数据输入端)`,`GND(接地)`。
### 74HC595
我们来看一下74HC595是什么样的
{% asset_img 74hc595.png %}
看懂这个芯片需要一点点的数字电子技术的知识,看下面这个表格
<!--more-->
| 符号 | 引脚 | 描述 |
| :------:| :------: | :------: |
| Q0~Q7 | 15,1~7 | 8位并行数据输出 |
| GND | 8 | 接地 |
| QA | 9 | 串行数据输出 |
| MR | 10 | 主复位(低电平) |
| SH_CP | 11 | 数据输入时钟 |
| ST_CP | 12 | 输出存储器锁存时钟线 |
| OE | 13 | 输出有效(低电平) |
| DS | 14 | 串行数据输入 |
| VCC | 16 | 3.3V/3.5V |
- `DS(14号脚)`是数据输入端,74HC595支持8位数据并行输出,但是数据输入只有DS一个脚,需要分8次输入;
- `OE(Out Enable)`是一个使能端,使用74HC595时需要将这个脚接地,若接高电平,那么Q0~Q7全部为0(低电平);
- `SH_CP(Shift register ClockPin)`是一个重要的引脚,此引脚的电平从低向高变(上升沿)时,芯片从`DS`脚读入数据(0/1);
- `ST_CP`,集满8位数据(其实并非严格集满,Q0~Q7本来就有电平),此引脚的上升沿将8位数据输出。
### 74HC595级联
一个74HC595可以并行输出8位数据,两个级联就可以并行输出16位数据,级联芯片时将第一片的`QA`脚接到第二片芯片的`DS`脚,`SH_CP`和`ST_CP`也同时接在一起,当第一片芯片的`SH_CP`第九次上升沿触发时,第一片芯片的第一个数据将会通过`QA`脚输送给第二片芯片的`DS`脚。
也就是说,芯片的顺序是看`QA`和`DS`脚的顺序的。
### 电路原理图
#### 四位数码管原理图
数码管分位共阳极和共阴极,我拿到的这个数码管是共阳极,也就是说,给相应的脚设为低电平,则数码管点亮。
{% asset_img 3.jpg %}
#### 四位数码管模块原理图
{% asset_img board.jpg %}
4位数码管模块使用两片74HC595级联来控制数码管,观察电路可知,第一片74HC595的低四位用来选中四位数码管,也即DIG.1,DIG.2,DIG.3,DIG.4,高四位留空不用。第二片的八位输出用来控制数码管显示的数字。
#### 数码管原理
{% asset_img 4.jpg %}
数码管内部其实是8个发光二极管,8个二极管排列成一个“8”字,控制二极管的明灭就可以控制数码管显示的数字。下面的表给出共阳极数码管的阵脚电平和显示数字对应关系(DP不亮)
| DP GFEDCBA | 显示(十进制) | 十六进制值 |
| :------:| :------: | :------: |
| 1 1000000 |0|0xC0|
| 1 1111001 |1|0xF9|
| 1 0100100 |2|0xA4|
| 1 0110000 |3|0xB0|
| 1 0011001 |4|0x99|
| 1 0010010 |5|0x92|
| 1 0000010 |6|0x82|
| 1 1111000 |7|0xF8|
| 1 0000000 |8|0x80|
| 1 0010000 |9|0x90|
### 树莓派引脚图
![http://shumeipai.nxez.com/wp-content/uploads/2015/03/rpi-pins-40-0.png](http://shumeipai.nxez.com/wp-content/uploads/2015/03/rpi-pins-40-0.png)
通过控制树莓派的`GPIO.X`引脚电平高低即可控制外部扩展设备。
看引脚的方式是树莓派有网线接口和 USB 接口的面朝下,GPIO 阵脚的那一面面向自己,左上角位 1 号脚,右下角为 40 号脚。
## 编码实现
代码都在 [https://github.com/BDZNH/4-BIT-LED-Digital-Tube-Moudle](https://github.com/BDZNH/4-BIT-LED-Digital-Tube-Moudle),下载之后,使用`make`命令编译,使用`sudo ./showtime `执行。
### 接线
| 四位数码管模块 | Raspeberry |
| :-----: | :-----: |
| VCC | 1 号脚(可改变) |
| GND | 9 号脚(可改变) |
| DIO | 12 号脚 |
| SCLK | 13 号脚 |
| RCLK | 15 号脚 |
## 演示
循环显示年份,日期,时间,只显示到分钟,不显示秒。
{% asset_img year.jpg 2019年 %}
{% asset_img md.jpg 1月10日 %}
{% asset_img hm.jpg 15:25 %}
<iframe src="//player.bilibili.com/player.html?aid=40384627&cid=70929081&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
## 参考
[1] 74HC595 的讲解,很详细,推荐观看[https://www.bilibili.com/video/av10007630/?p=14](https://www.bilibili.com/video/av10007630/?p=14)
[2] wiringPi 库的解说,也很详细,推荐收藏[https://www.cnblogs.com/lulipro/p/5992172.html](https://www.cnblogs.com/lulipro/p/5992172.html)
]]></content>
</entry>
<entry>
<title>今日头条 ANR 优化实践系列 - 监控工具与分析思路</title>
<url>/2024/08/11/ANR-page-1/</url>
<content><![CDATA[
前言:这篇文章是一篇转载文章用,用来为后面的一些文章做铺垫。
转载链接为 [今日头条 ANR 优化实践系列 - 监控工具与分析思路](https://mp.weixin.qq.com/s/_Z6GdGRVWq-_JXf5Fs6fsw),如有侵权,请在 GitHub 提 Issue 联系我。
<!-- more -->
在前文,我们对[ANR 设计原理及影响因素](https://mp.weixin.qq.com/s/ApNSEWxQdM19QoCNijagtg)进行了介绍,并对影响 ANR 的不同场景进行归类。但是依靠现有的系统日志,不足以完成复杂场景的问题归因,而且有些信息从应用侧无法获取,这就导致很多线上问题更加棘手;因此我们在应用侧探索了新的监控能力,以弥补信息获取不足的短板。同时对日常分析过程中用到日志信息和分析思路进行总结,以帮忙大家更好的掌握分析技巧,下面我们就来看看相关实现。
# Raster 监控工具
俗话说:“工欲善其事,必先利其器”,日常分析 ANR 问题也是如此,一个好的监控工具不仅可以帮助我们在解决常规问题时达到锦上添花的效果,在面对线上复杂隐蔽的问题时,也能为我们打开视野,提供更多线索和思路。
## 工具介绍:
该工具主要是在主线程消息调度过程进行监控,并按照一定策略聚合,以保证监控工具本身对应用性能和内存抖动影响降至最低。同时对应用四大组件消息执行过程进行监控,便于对这类消息的调度及耗时情况进行跟踪和记录。另外对当前正在调度的消息及消息队列中待调度消息进行统计,从而在发生问题时,可以回放主线程的整体调度情况。此外,我们将系统服务的 CheckTime 机制迁移到应用侧,应用为线程 CheckTime 机制,以便于系统信息不足时,从线程调度及时性推测过去一段时间系统负载和调度情况。
因此该工具用一句话来概括就是:由点到面,回放过去,现在和将来。
因其实现原理和消息聚合后的效果,直观展示主线程调度过程长短不一的耗时片段,犹如一道道光栅,故将该工具命名为 Raster[ˈræstər]。
## 监控工具由来
例如下图,是线下遇到的 ANR 问题,从手机端获取的 Trace 日志,可以看到从主线程堆栈上基本得不到太多有效信息。
{% asset_img 1.png %}
继续从 Trace 中分析其它信息,包含了各个进程的虚拟机和线程状态信息,以及 ANR 之前或之后一段时间,CPU 使用率比较高的进程乃至系统负载(CPU,IO)的相关信息等等,如下图:
{% asset_img 2.png %}
但是从这些信息中,相信很多同学都很难再进一步分析,因为这些信息只是列举了当前各个进程或线程的状态,并没有很好的监控和记录影响这些指标的过程。而现实中这类场景的问题,每天都在线上大量发生。那么针对这种情况该如何更好的解决呢?下面就来介绍一下我们是如何应对的。
## 消息调度监控
在Android 系统的 ANR 设计原理及影响因素一文中,我们讲到,ANR 问题很多场景都是历史消息耗时较长并不断累加后导致的,但是在 ANR 发生时我们并不知道之前都调度了哪些消息,如果可以监控每次消息调度的耗时并记录,当发生 ANR 时,获取这些记录信息,并能计算出当前正在执行消息的耗时,是不是就可以清晰的知道 ANR 发生前主线程都发生了什么?按照这个思路,整理出如下示意图:
{% asset_img 3.png %}
但是通过上面示意图并结合实际业务场景,我们发现,对于大多数业务消息,单次耗时都很少,如果每个消息都单独记录,要想跟踪记录 ANR 前 10S 甚至更长时间范围内的所有消息,可能需要成千上万条记录,这个显然是不合理的,而且这么多的消息也不方便我们后续查看。
### 消息聚合分类
联想到实际业务场景很多情况都是耗时较少的消息,而在排查这类问题过程耗时较少的消息基本是可以直接忽略的,因此我们是不是可以对这类消息按照一定条件进行聚合,将一段时间以内的消息进行累加计算,如果累计耗时超过我们规定的阈值,那么就将这些消息的数量和累计耗时合并成一条记录,如 16 个消息累计耗时刚好超过 300ms,则生成一条记录。以此类推存储 100 条的话,就能监控到 ANR 发生前 30S 主线程的调度历史了(实际可能是大于 15S,至于为何是这个范围,我们会在后面说明),如此一来就可以较好的解决大量记录和频繁更新带来的内存抖动问题。
根据上面的思路,我们对消息监控及记录又进一步的进行聚合优化和关键消息过滤;总结来看,分为以下几种类型:
### 多消息聚合:
该场景主要用于主线程连续调度多个消息,并且每个消息耗时都很少的情况下,将这些消息耗时累加,直到这些消息累计耗时超过设置的阈值,则汇总并生成一条记录,并在这条记录中标明本次聚合总共调度了多少个消息。按照消息聚合的思路,发生问题时主线程消息调度示意图如下:
{% asset_img 4.png %}
### 消息聚合拆分:
针对上面多消息聚合策略,会存在一些特殊情况,例如在将多个消息进行累计统计过程中,如果前 N 次消息调度结束后,累计耗时都没有超过阈值,但是调度完下一个消息之后,发现累计耗时超过阈值,并且还明显超出,如设置阈值是 300ms,但是前 N 个消息累计 200ms,加上本次消息累计耗时达到了 900ms,那么这种情况,我们明显知道是最后一次消息耗时严重,因此需要将本次消息单独记录,并标记其耗时和相关信息,同时将之前 N 次消息调度耗时和消息数聚合在一起并单独记录,这种场景相当于一次生成 2 条记录。
为了考虑监控工具对性能的影响,我们只在每轮统计需要保存时,更新线程 cpu 执行时间,如果发生消息聚合拆分的场景,会默认前一条记录的 cpu 时间为-1,因为本条记录并不是我们重点关注的,所以会把本轮统计的 cpu 时间全部归到后一条消息。
在一些极端场景下,如本轮监控第一个消息执行耗时为 1ms,但是加上本次消息耗时,累计超过 600ms,因此两次消息累计耗时远大于设定的阈值 300ms,那么就需要对本次耗时严重的消息单独记录,而前面那个 1ms 的消息也需要被单独进行记录;类似情形如此反复,就会出现上面说的保存 100 条记录,整体监控可回溯的时长区间存在波动的情况;该场景的示意图如下:
{% asset_img 5.png %}
### 关键消息聚合:
除了上面单次耗时严重的消息需要拆分并单独记录之外,还有一类消息也需要我们单独标记,以达到更好的识别,那就是可能会引起 ANR 的应用组件,如 Activity,Service,Receiver,Provider 等等。为了监控这几种组件的执行过程,我们需要对 ActivityThread.H 的消息调度进行监控,当发现上面这些组件有关的消息在执行时,不管其耗时多少,都对其进行单独记录,并将之前监控的一个或多个消息也保存为一条记录。该场景的示意图如下:
{% asset_img 6.png %}
### IDLE 场景聚合:
熟悉消息队列的同学都知道,主线程是基于消息队列的方式进行调度,在每次消息调度完成之后都会从消息队列读取下一个待调度的消息,如果消息队列没有消息,或者下一个消息没有到设定的调度时间,并且也没有 IDLE 消息等待调度,那么主线程将会进入 IDLE 状态,如下示意图:
{% asset_img 7.png %}
正是因为上面的调度逻辑,使得主线程在消息调度过程中会多次进入 IDLE 状态,而这个过程也涉及到线程上下文切换(如:Java 环境切换到 Native 环境),会去检测是否有挂起请求,所以对调用频繁的接口来说,会在 ANR 发生时被命中,理论上调用越频繁的接口被命中的概率越大,如下图堆栈:
{% asset_img 8.png %}
但是上面这种场景的 IDLE 停留时长可长可短,如果按照完全上面那几类消息聚合策略,多个消息连续聚合的话,可能会把这类场景也给聚合进来,一定程度造成干扰,这显然不是我们想要的。为此需要进一步优化,在每次消息调度结束后,获取当前时间,在下次消息调度开始前,再次获取当前时间,并统计距离上次消息调度结束的间隔时长。如果间隔较长,那么也需要单独记录,如果间隔时间较短,我们认为可以忽略,并将其合并到之前统计的消息一起跟踪,到这里就完成了各类场景的监控和归类;该场景的示意图如下:
{% asset_img 9.png %}
### 耗时消息堆栈采样:
在上面重点讲述了主线程消息调度过程的监控和聚合策略,便于发生 ANR,在线下进行回放。但是那些耗时较长的消息,仅仅知道其耗时和消息 tag 是远远不够的,因为每个消息内部的业务逻辑对于我们来说都是黑盒,各个接口耗时也存在很多不确定性,如锁等待、Binder 通信、IO 等系统调用。
因此需要知道这些耗时消息内部接口的耗时情况,我们选取了 2 种方案进行对比:
第一种方案:是对每个函数进行插桩,在进入和退出过程统计其耗时,并进行聚合和汇总。该方案的优点是可以精确的知道每个函数的真实耗时,缺点是很影响包体积和性能,而且不利于其他产品高效复用。
第二种方案,在每个消息开始执行时,触发子线程的超时监控,如果在超时之后本次消息还没执行结束,则抓取主线程堆栈,并继续对该消息设置下一次超时监控,直到该消息执行结束并取消本轮监控。如果在触发超时之前已经执行完毕,则取消本次监控并在下次消息开始执行时,再次设置超时监控,但是因为大部分消息耗时都很少,如果每次都频繁设置和取消,将会带来性能影响,因此我们对此进行优化,采用系统 ANR 超时监控相同的时间对齐方案,具体来说就是:
以消息开始时间加上超时时长为目标超时时间,每次超时时间到了之后,检查当前时间是否大于或等于目标时间,如果满足,则说明目标时间没有更新,也就是说本次消息没结束,则抓取堆栈。如果每次超时之后,检查当前时间小于目标时间,则说明上次消息执行结束,新的消息开始执行并更新了目标超时时间,这时异步监控需对齐目标超时,再次设置超时监控,如此往复。
根据上面的思路,整理流程图如下:
{% asset_img 10.png %}
需要注意的是,消息采样堆栈的超时时长不可设置太短,否则频繁抓取堆栈对主线程性能影响较大,同时也不能设置太长,否则会因为采样过低导致数据置信度降低;具体时长根据每个产品复杂度灵活调整即可。
### 监控正在调度消息及耗时:
除了监控 ANR 发生之前主线程历史消息调度及耗时之外,也需要知道 ANR 发生时正在调度的消息及其耗时,以便于在看到 ANR 的 Trace 堆栈时,可以清晰的知道当前 Trace 逻辑到底执行了多长时间,帮忙我们排除干扰,快速定位;借助这个监控可以很好的回答大家,ANR 发生时当前 Trace 堆栈是否耗时以及耗时多久的问题,避免陷入“Trace 堆栈”误区。
### 获取 Pending 消息:
同时除了监控主线程历史消息调度及耗时之外,也需要在 ANR 发生时,获取消息队列中待调度的消息,为我们分析问题时提供更多线索,如:
- 消息队列中待调度消息是否被 Block 以及被 Block 时长,根据 Block 时长可以推测主线程的繁忙程度;
- 可以观察消息队列中是否存在发生 ANR 的应用组件消息,如 Service 消息,以及其在待调度消息队列中的位置和 Block 时长;
- 可以观察消息队列中都有哪些消息,这些消息是否有一定规律,如大量重复消息,如果有大量重复消息,则说明很有可能与该消息相关的业务逻辑发生异常,频繁和主线程交互(后面的案例分析中我们也会有介绍)。
{% asset_img 11.png %}
之前我们讲到,对于一次消息调度,它的耗时可以从两个维度进行统计,即 Wall Duration 和 Cpu Duration,通过这两个维度的统计,可以帮助我们更好的推测一次严重耗时,是执行大量业务逻辑还是处于等待或被抢占的情况。如果是后者,那么可以看到这类消息的 Wall Duration 和 Cpu Duration 比例会比较大,当然如何更好更全面的区分一次消息耗时是等待较多还是线程调度被抢占,我们将会在后面结合其他参考信息进行介绍。
### 完整示意图
在这里我们再把 Cpu Duration 耗时也给统计之后,那么整个有关主线程完整的消息调度监控功能就基本完成了。示意图如下:
{% asset_img 12.png %}
通过这个消息调度监控工具,我们就可以很清晰的看到发生 ANR 时,主线程历史消息调度情况;当前正在调度消息耗时,以及消息队列待调度消息及相关信息;而且利用这个监控工具,一眼便知 ANR 发生时主线程 Trace 实际耗时情况,因此很好解决了部分同学对当前堆栈是否耗时以及耗时多久的疑问。
从上面介绍可以看出,为了重点标记单次耗时消息和关键消息,我们使用了多种聚合策略,因此监控过程记录的信息可能会代表不同类型的消息,为了便于区分,我们在可视化展示时加上 Type 标识,便于区别。
# 应用示例:
例如下图,从 Trace 日志可以看到,ANR 发生时主线程被 Block 在 Binder 通信过程,可能很多同学第一反应是 WMS 服务没有及时响应 Binder 请求导致的问题。
{% asset_img 13.png %}
但是再结合下面的消息调度监控来核实一下,我们发现当前调度的消息 Wall duration 只有 44ms,而在该消息之前有两次历史消息耗时比较严重,分别为 2166ms,3277ms,也就是说本次 Binder 调用耗时并不严重,真正的问题是前面 2 次消息耗时较长,影响了后续消息调度,只有同时解决这 2 个消息耗时严重问题,该 ANR 问题才可能解决。
{% asset_img 14.png %}
如果没有消息调度监控工具,上去就盲目的分析当前逻辑调用的 IPC 问题,可能就犯了方向性的错误,掉入“Trace 堆栈”陷阱中。
接下来再来看一个发生在线上的另外一个实例,从下图可以看到主线程正在调度的消息耗时超过 1S,但是在此之前的另一个历史消息耗时长达 9828ms。继续观察下图消息队列待调度的消息状态(灰色示意),可以看到第一个待调度的消息被 Block 了 14S 之久。由此我们可以知道 ANR 消息之前的这个历史消息,才是导致 ANR 的罪魁祸首,当然这个正在执行的消息也需要优化一下性能,因为我们在前面说过:“发生 ANR 时,没有一个消息是无辜的。”
{% asset_img 15.png %}
正是因为有了上面这些监控能力,让我们在日常面对 Trace 日志中的业务逻辑是否耗时以及耗时多久的困惑,瞬间就会变得清晰起来。
# Checktime
## Checktime 背景介绍:
Checktime 是 Android 系统针对一些系统服务(AMS,InputService 等)中高频访问的接口,执行时间的监控,当这类接口真实耗时超过预期值将会给出提示信息,此类设计为了在真实环境监测进程被调度和响应能力的一种结果反馈。具体实现是,在每个函数执行前和执行后分析获取当前系统时间,并计算差值,如果该接口耗时超过其设定的阈值,则会触发“slow operation”的信息提醒,部分代码实现如下:
{% asset_img 16.png %}
Checktime 逻辑很简单,用当前系统时间减去对比时间,如果超过 50ms,则给出 Waring 日志提示。
{% asset_img 17.png %}
我们在分析线下问题,或者在系统层面分析这类问题时,经常会在 logcat 中看到这类消息,但是对于线上的三方应用来说,因为权限问题无法获取系统日志,只能自己实现了。
## 线程 Checktime:
了解完系统 Checktime 设计思路及实现之后,我们就可以在应用层实现类似的功能了。通过借助其它子线程的周期检测机制,`在每次调度前获取当前系统时间,然后减去我们设置延迟的时间,即可得到本次线程调度前的真实间隔时间,如设置线程每隔 300ms 调度一次,结果发现实际响应时间间隔有时会超过 300ms,如果偏差越大,则说明线程没有被及时调度,进一步反映系统响应能力变差`。
通过这样的方式,即使在线上环境获取不到系统日志,也可以从侧面反映不同时段系统负载对线程调度影响,如下图示意,当连续发生多次严重 Delay 时,说明线程调度受到了影响。
{% asset_img 18.png %}
## 小结:
通过上述监控能力,我们可以清晰的知道 ANR 发生时主线程历史消息调度以及耗时严重消息的采样堆栈,同时可以知道正在执行消息的耗时,以及消息队列待中调度消息的状态。同时通过线程 CheckTime 机制从侧面反映线程调度响应能力,由此完成了应用侧监控信息从点到面的覆盖。但是在面对 ANR 问题时,只有这个监控,是远远不够的,需要结合其他信息整体分析,以应对更为复杂的系统环境。下面就结合监控工具来介绍一下 ANR 问题的分析思路。
# ANR 分析思路:
在介绍分析思路之前,我们先来说一下分析这类问题需要用到哪些日志,当然在不同的环境下,获取信息能力会有很大差别,如线下环境和线上环境,应用侧和系统角度都有差异;这里我们会将我们日常排查问题常用的信息都介绍一下,便于大家更好的理解,主要包括以下几种:
- Trace 日志
- AnrInfo
- Kernel 日志
- Logcat 日志
- Meminfo 日志
- Raster 监控工具
对于应用侧来说,在线上环境可能只能拿到当前进程内部的线程堆栈(取决于实现原理,参见:Android 系统的 ANR 设计原理及影响因素)以及 ANR Info 信息。在系统侧,几乎能获取到上面的所有信息,对于这类问题获取的信息越多,分析定位成功率就越大,例如可以利用完整的 Trace 日志,分析跨进程 Block 或死锁问题,系统内存或 IO 紧张程度等等,甚至可以知道硬件状态,如低电状态,硬件频率(CPU,IO,GPU)等等。
## 关键信息解读:
在这里我们把上面列举的日志进行提取并解读,以便于大家在日常开发和面对线上问题,根据当前获取的信息进行参考。
## Trace 信息
在前文Android 系统的 ANR 设计原理及影响因素中,我们讲到了在发生 ANR 之后,系统会 Dump 当前进程以及关键进程的线程堆栈,状态(红框所示关键信息,稍后详细说明),示例如下:
{% asset_img 19.png %}
上面的日志包含很多信息,这里将常用的关键信息进行说明,如下:
- 线程堆栈:
这个比较好理解,也就是发生 ANR 时,线程正在执行的逻辑,但是很多场景下,获取的堆栈耗时并不长,原因详见Android 系统的 ANR 设计原理及影响因素。
- 线程状态:
见上图“state=xxx”,表示当前线程工作状态,Running 表示当前线程正在被 CPU 调度,Runnable 表示线程已经 Ready 等待 CPU 调度,如上图 SignalCatcher 线程状态。Native 态则表示线程由 Java 环境进入到 Native 环境,可能在执行 Native 逻辑,也可能是进入等待状态;Waiting 表示处于空闲等待状态。除此之外还有 Sleep,Blocked 状态等等;
- 线程耗时:
见上图“utmXXX,stmXXX”,表示该线程从创建到现在,被 CPU 调度的真实运行时长,不包括线程等待或者 Sleep 耗时,其中线程 CPU 耗时又可以进一步分为用户空间耗时(utm)和系统空间耗时(stm),这里的单位是 jiffies,当 HZ=100 时,1utm 等于 10ms。
- utm: Java 层和 Native 层非 Kernel 层系统调用的逻辑,执行时间都会被统计为用户空间耗时;
- stm: 即系统空间耗时,一般调用 Kernel 层 API 过程中会进行空间切换,由用户空间切换到 Kernel 空间,在 Kernel 层执行的逻辑耗时会被统计为 stm,如文件操作,open,write,read 等等;
- core:最后执行这个线程的 cpu 核的序号。
- 线程优先级:
nice: 该值越低,代表当前线程优先级越高,理论上享受的 CPU 调度能力也越强。对于应用进程(线程)来说,nice 范围基本在 100~139。随着应用所在前后台不同场景,系统会对进程优先级进行调整,厂商可能也会开启 cpu quota 等功能去限制调度能力;
- 调度态:
schedstat: 参见“schedstat=( 1813617862 14167238 546 )”,分别表示线程 CPU 执行时长(单位 ns),等待时长,Switch 次数。
## AnrInfo 信息
除了 Trace 之外,系统会在发生 ANR 时获取一些系统状态,如 ANR 问题发生之前和之后的系统负载以及 Top 进程和关键进程 CPU 使用率。这些信息如果在本地环境可以从 Logcat 日志中拿到,也可以在应用侧通过系统提供的 API 获取(参见:Android 系统的 ANR 设计原理及影响因素),Anr Info 节选部分信息如下:
{% asset_img 20.png %}
对于上图信息,主要对以下几部分关键信息进行介绍:
- ANR 类型(longMsg):
表示当前是哪种类型的消息或应用组件导致的 ANR,如 Input,Receiver,Service 等等。
- 系统负载(Load):
表示不同时间段的系统整体负载,如:"Load:45.53 / 27.94 / 19.57",分布代表 ANR 发生前 1 分钟,前 5 分钟,前 15 分钟各个时间段系统 CPU 负载,具体数值代表单位时间等待系统调度的任务数(可以理解为线程)。如果这个数值过高,则表示当前系统中面临 CPU 或 IO 竞争,此时,普通进程或线程调度将会受到影响。如果手机处于温度过高或低电等场景,系统会进行限频,甚至限核,此时系统调度能力也会受到影响。
此外,可以将这些时间段的负载和应用进程启动时长进行关联。如果进程刚启动 1 分钟,但是从 Load 数据看到前 5 分钟,甚至前 15 分钟系统负载已经很高,那很大程度说明本次 ANR 在很大程度会受到系统环境影响。
- 进程 CPU 使用率:
如上图,表示当前 ANR 问题发生之前(CPU usage from XXX to XXX ago)或发生之后(CPU usage from XXX to XXX later)一段时间内,都有哪些进程占用 CPU 较高,并列出这些进程的 user,kernel 的 CPU 占比。当然很多场景会出现 system_server 进程 CPU 占比较高的现象,针对这个进程需要视情况而定,至于 system_server 进程 CPU 占比为何普遍较高,参见:Android 系统的 ANR 设计原理及影响因素。
minor 表示次要页错误,文件或其它内存被加载到内存后,但是没有被映射到当前进程,通过内核访问时,会触发一次 Page Fault。如果访问的内容还没有加载到内存,那么会触发 major,所以对比可以看到,major 的系统开销会比 minor 大很多。
- 关键进程:
kswapd: 是 linux 中用于页面回收的内核线程,主要用来维护可用内存与文件缓存的平衡,以追求性能最大化,当该线程 CPU 占用过高,说明系统可用内存紧张,或者内存碎片化严重,需要进行 file cache 回写或者内存交换(交换到磁盘),线程 CPU 过高则系统整体性能将会明显下降,进而影响所有应用调度。
mmcqd: 内核线程,主要作用是把上层的 IO 请求进行统一管理和转发到 Driver 层,当该线程 CPU 占用过高,说明系统存在大量文件读写,当然如果内存紧张也会触发文件回写和内存交换到磁盘,所以 kswapd 和 mmcqd 经常是同步出现的。
- 系统 CPU 分布:
如下图,反映一段时间内,系统整体 CPU 使用率,以及 user,kernel,iowait 方向的 CPU 占比,如果发生大量文件读写或内存紧张的场景,则 iowait 占比较高,这个时候则要进一步观察上述进程的 kernel 空间 CPU 使用情况,并通过进程 CPU 使用,再进一步对比各个线程的 CPU 使用,找出占比最大的一个或一类线程。
{%asset_img 21.png %}
- Logcat 日志:
在 log 日志中,我们除了可以观察业务信息之外,还有一些关键字也可以帮我们去推测当前系统性能是否遇到问题,如下图, “`Slow operation`”,“`Slow delivery`” 等等。
{%asset_img 22.png %}
- Slow operation
Android 系统在一些频繁调用的接口中,分别在方法前后利用 checktime 检测,以判断本次函数执行耗时是否超过设定阈值,通常这些值都会设置的较为宽松,如果实际耗时超过设置阈值,则会给出“Slow XXX”提示,表示系统进程调度受到了影响,一般来说系统进程优先级比较高,如果系统进程调度都受到了影响,那么则反映了这段时间内系统性能很有可能发生了问题。
- Kernel 日志:
对于应用侧来说,这类日志基本是拿不到的,但是如下是在线下测试或者从事系统开发的同学,可以通过 dmesg 命令进行查看。对于 kernel 日志,我们主要分析的是 lowmemkiller 相关信息,如下图:
{%asset_img 23.png %}
- Lowmemkiller:
从事性能(内存)优化的同学对该模块都比较熟悉,主要是用来监控和管理系统可用内存,当可用内存紧张时,从 kernel 层强制 Kill 一些低优先级的应用,以达到调节系统内存的目的。而选择哪些应用,则主要参考进程优先级(oom_score_adj),这个优先级是 AMS 服务根据应用当前的状态,如前台还是后台,以及进程存活的应用组件类型而计算出来的。例如:对于用户感知比较明显的前台应用,优先级肯定是最高的,此外还有一些系统服务,和后台服务(播放器场景)优先级也会比较高。当然厂商也对此进行了大量的定制(优化),以防止三方应用利用系统设计漏洞,将自身进程设置太高优先级进而达到保活目的。
- 消息调度时序图:
{%asset_img 24.png %}
如上图,在我们分析完系统日志之后,会进一步的锁定或缩小范围,但是最终我们还是要回归到主线程进一步的分析 Trace 堆栈的业务逻辑以及耗时情况,以便于我们更加清晰的知道正在调度的消息持续了多长时间。但是很多情况当前 Trace 堆栈并不是我们期待的答案,因此需要进一步的确认 ANR 之前主线程的调度信息,评估历史消息对后续消息调度的影响,便于我们寻找“真凶”。
当然,有时也需要进一步的参考消息队列中待调度消息,在这些消息里面,除了可以看到 ANR 时对应的应用组件被 Block 的时长之外,还可以了解一下都有哪些消息,这些消息的特征有时对于我们分析问题也会提供有力的证据和方向。
## 分析思路
在上面我们对各类日志的关键信息进行了基本释义,下面就来介绍一下,当我们日常遇到 ANR 问题时,是如何分析的,总结思路如下:
- 分析堆栈,看看是否存在明显业务问题(如死锁,业务严重耗时等等),如果无上述明显问题,则进一步通过 ANR Info 观察系统负载是否过高,进而导致整体性能较差,如 CPU,Mem,IO。然后再进一步分析是本进程还是其它进程导致,最后再分析进程内部分析对比各个线程 CPU 占比,找出可疑线程。
- 综合上述信息,利用监控工具收集的信息,观察和找出 ANR 发生前一段时间内,主线程耗时较长的消息都有哪些,并查看这些耗时较长的消息执行过程中采样堆栈,根据堆栈聚合展示,进一步的对比当前耗时严重的接口或业务逻辑。
以上分析思路,进一步细分的话,可以分为以下几个步骤:
- 一看 Trace:
- 死锁堆栈: 观察 Trace 堆栈,确认是否有明显问题,如主线程是否与其他线程发生死锁,如果是进程内部发生了死锁,那么恭喜,这类问题就清晰多了,只需找到与当前线程死锁的线程,问题即可解决;
- 业务堆栈: 观察通过 Trace 堆栈,发现当前主线程堆栈正在执行业务逻辑,你找到对应的业务同学,他承认该业务逻辑确实存在性能问题,那么恭喜,你很有可能解决了该问题,为什么只是有可能解决该问题呢?因为有些问题取决于技术栈或框架设计,无法在短时间内解决。如果业务同学反馈当前业务很简单,基本不怎么耗时,而这种场景也是日常经常遇到的一类问题,那么就可能需要借助我们的监控工具,追溯历史消息耗时情况了;
- IPC Block 堆栈: 观察通过 Trace 堆栈,发现主线程堆栈是在跨进程(Binder)通信,那么这个情况并不能当即下定论就是 IPC block 导致,实际情况也有可能是刚发送 Binder 请求不久,以及想要进一步的分析定位,这时也需要借助我们的自研监控工具了;
- 系统堆栈: 通过观察 Trace,发现当前堆栈只是简单的系统堆栈,想要搞清楚是否发生严重耗时,以及进一步的分析定位,如我们常见的 NativePollOnce 场景,那么也需要借助我们的自研监控工具进一步确认了。
- 二看关键字:Load,CPU,Slow Operation,Kswapd,Mmcqd,Kwork,Lowmemkiller 等等
刚才我们介绍到,上面这些关键字是反应系统 CPU,Mem,IO 负载的关键信息,在分析完主线程堆栈信息之后,还需要进一步在 ANRInfo,logcat 或 Kernel 日志中搜索这些关键字,并根据这些关键字当前数值,判断当前系统是否存在资源(CPU,Mem,IO)紧张的情况;
- 三看系统负载分布:观察系统整体负载:User,Sys,IOWait
通过观察系统负载,则可以进一步明确是 CPU 资源紧张,还是 IO 资源紧张;如果系统负载过高,一定是有某个进程或多个进程引起的。反之系统负载过高又会影响到所有进程调度性能。通过观察 User,Sys 的 CPU 占比,可以进一步发分析当前负载过高是发生在应用空间,还是系统空间,如大量调用逻辑(如文件读写,内存紧张导致系统不断回收内存等等),知道这些之后,排查方向又会进一步缩小范围。
- 四看进程 CPU:观察 Top 进程的 CPU 占比
从上面分析,在我们知道当前系统负载过高,是发生在用户空间还是内核空间之后,那么我们就要通过 Anrinfo 的提供的进程 CPU 列表,进一步锁定是哪个(些)进程导致的,这时则要进一步的观察每个进程的 CPU 占比,以及进程内部 user,sys 占比。
- 在分析进程 CPU 占比过程,有一个关键的信息,要看统计这些进程 CPU 过高的场景是发生在 ANR 之前的一段时间还是之后一段时间,如下图表示 ANR 之前 4339ms 到 22895ms 时间内进程的 CPU 使用率。
{% asset_img 25.png %}
- 五看 CPU 占比定线程 :对比各线程 CPU 占比,以及线程内部 user 和 kernel 占比
在通过系统负载(user,sys,iowait)锁定方向之后,又通过进程列表锁定目标进程,那么接下来我们就可以从目标进程内部分析各个线程的(utm,stm),进一步分析是哪个线程有问题了。
在 Trace 日志的线程信息里可以清晰的看到每个线程的 utm,stm 耗时。至此我们就完成了从系统到进程,再到进程内部线程方向的负载分析和排查。当然,有时候可能导致系统高负载的不是当前进程,而是其他进程导致,这时同样会影响其他进程,进而导致 ANR。
- 六看消息调度锁定细节 :
在分析和明确系统负载是否正常,以及负载过高是哪个进程引起的结论之后,接下来便要通过我们的监控工具,进一步排查是当前消息调度耗时导致,历史消息调度耗时导致,还是消息过于频繁导致。同时通过我们的线程 CheckTime 调度情况分析当前进程的 CPU 调度是否及时以及影响程度,在锁定上述场景之后,再进一步分析耗时消息的采样堆栈,才算找到解决问题的终极之钥。当然耗时消息内部可能存在一个或多个耗时较长的函数接口,或者会有多个消息存在耗时较长的函数接口,这就是我们前文中提到的:“发生 ANR 时,没有一个消息是无辜的”
## 更多信息:
除了上面的一些信息,我们还可以结合 logcat 日志分析 ANR 之前的一些信息,查看是否存在业务侧或系统侧的异常输出,如搜索“Slow operation”,"Slow delivery"等关键字。也可以观察当前进程和系统进程是否存在频繁 GC 等等,以帮忙我们更全面的分析系统状态。
## 总结:
上面我们重点介绍了基于主线程消息调度的监控工具,实现了“由点到面”的监控能力,以便于发生 ANR 问题时,可以更加清晰直观的整体评估主线程的“过去,现在和将来”。同时结合日常实践,介绍了在应用侧分析 ANR 问题经常用到的日志信息和分析思路。
目前,Raster 监控工具因为其很好的提升了问题定位效率和成功率,成为 ANR 问题分析利器,并融合到公司性能稳定性监控平台,为公司众多产品广泛使用。接下来我们将利用该工具并结合上面的分析思路,讲一讲实际工作中遇到不同类型的 ANR 问题时,是如何快速分析和定位问题的。
]]></content>
<tags>
<tag>Android</tag>
<tag>ANR</tag>
</tags>
</entry>
<entry>
<title>消息监控工具的一种实现思路</title>
<url>/2024/08/11/ANR-page-2/</url>
<content><![CDATA[
在 [今日头条 ANR 优化实践系列 - 监控工具与分析思路](#../ANR-page-1) 文中描述到使用了一种名为“Raster”的工具来做消息监控,对于ANR比较有用。但是在网络上没有搜到这个工具的具体实现方案。
本文来设计一种类似的消息监控的实现思路。
<!-- more -->
这里大概说一下思路。
1. 记录主线程Looper的消息分发记录
2. 当 Signal Catcher线程收到QUIT消息的时候,Jni回调到ActivityThread,获取记录好的消息分发记录
3. 写入 trace 文件。
下面是一种参考实现方式
```diff
commit cd0214ccd7e58224723eb3d581ef67afc3d43161
Author: BDZNH
Date: Mon Aug 19 22:34:37 2024 +0800
anr catch sample
diff --git a/art/runtime/signal_catcher.cc b/art/runtime/signal_catcher.cc
index f76a0d0b7b..c4414f1396 100644
--- a/art/runtime/signal_catcher.cc
+++ b/art/runtime/signal_catcher.cc
@@ -114,6 +114,36 @@ void SignalCatcher::Output(const std::string& s) {
}
}
+void testCallActivityThread(std::ostringstream& os) {
+ LOG(INFO) << "start testCallActivityThread";
+ Thread* self = Thread::Current();
+ JNIEnv* env = self->GetJniEnv();
+ jclass clazz = env->FindClass("android/app/ActivityThread");
+ if(clazz == nullptr) {
+ LOG(ERROR) << "can't find class android/app/ActivityThread";
+ return;
+ }
+ jmethodID onSignalQuitMethod = env->GetStaticMethodID(clazz,"onSignalQuit","()Ljava/lang/String;");
+ if(onSignalQuitMethod == nullptr) {
+ LOG(ERROR) << "can't find method onSignalQuitMethod";
+ return;
+ }
+ LOG(INFO) << "call android.app.ActivityThread.onSignalQuit";
+ LOG(INFO) << "clazz: " << (void*)clazz << " method: " << (void*)onSignalQuitMethod;
+ jstring retstring = (jstring)(env->CallStaticObjectMethod(clazz,onSignalQuitMethod));
+ if(retstring != nullptr) {
+ const char* str = env->GetStringUTFChars(retstring,NULL);
+ if(str != nullptr) {
+ // LOG(INFO) << str ;
+ os << str << "\n";
+ } else {
+ LOG(ERROR) << "Java api return null ptr";
+ }
+ } else {
+ LOG(ERROR) << "Java api return null ptr";
+ }
+}
+
void SignalCatcher::HandleSigQuit() {
Runtime* runtime = Runtime::Current();
std::ostringstream os;
@@ -130,6 +160,9 @@ void SignalCatcher::HandleSigQuit() {
os << "Build type: " << (kIsDebugBuild ? "debug" : "optimized") << "\n";
+ os << ">>>>>>>>>> Begin dump main thread handler history" << "\n";
+ testCallActivityThread(os);
+ os << ">>>>>>>>>> finish dump main thread handler history" << "\n";
runtime->DumpForSigQuit(os);
if ((false)) {
diff --git a/frameworks/base/core/java/android/app/ActivityThread.java b/frameworks/base/core/java/android/app/ActivityThread.java
index 8af74edb88..dfce7abfe2 100644
--- a/frameworks/base/core/java/android/app/ActivityThread.java
+++ b/frameworks/base/core/java/android/app/ActivityThread.java
@@ -7818,6 +7818,20 @@ public final class ActivityThread extends ClientTransactionHandler
}
}
+ /**
+ * @hide
+ */
+ public static String onSignalQuit() {
+ Log.i("HSP","onSignalQuit call begin");
+ StringBuilder sb = new StringBuilder();
+ Looper.getMainLooper().dump((str)->{
+ sb.append(str);
+ sb.append("\n");
+ }," ");
+ Log.i("HSP","onSignalQuit call end");
+ return sb.toString();
+ }
+
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
@@ -7860,11 +7874,17 @@ public final class ActivityThread extends ClientTransactionHandler
sMainThreadHandler = thread.getHandler();
}
- if (false) {
+ if(SystemProperties.getBoolean("persist.sys.debug.looper.history",false)) {
+ Looper.myLooper().enableDispatchHistory();
+ }
+
+ if (SystemProperties.getBoolean("persist.sys.debug.handler",false)) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
+ Looper.myLooper().setTraceTag(Trace.TRACE_TAG_ALWAYS);
}
+
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
diff --git a/frameworks/base/core/java/android/os/Looper.java b/frameworks/base/core/java/android/os/Looper.java
index 8c98362fa9..8f6144d988 100644
--- a/frameworks/base/core/java/android/os/Looper.java
+++ b/frameworks/base/core/java/android/os/Looper.java
@@ -152,6 +152,15 @@ public final class Looper {
sObserver = observer;
}
+ MessageDispatchRecorder mMessageRecorder;
+
+ /**
+ * @hide
+ */
+ public void enableDispatchHistory(){
+ mMessageRecorder = new MessageDispatchRecorder();
+ }
+
/**
* Poll and deliver single message, return true if the outer loop should continue.
*/
@@ -186,8 +195,14 @@ public final class Looper {
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
+
+ if(me.mMessageRecorder != null) {
+ me.mMessageRecorder.beginRecord(msg);
+ }
+ String traceHandleMsg = msg.target.getTraceName(msg);
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
+ Slog.v("TraceHandler","Dispatching to" + traceHandleMsg);
}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
@@ -232,10 +247,16 @@ public final class Looper {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
+ if(me.mMessageRecorder != null) {
+ me.mMessageRecorder.endRecord();
+ }
+
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
+ Slog.v("TraceHandler","Finished Dispatching to" + traceHandleMsg);
}
+
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
@@ -422,6 +443,9 @@ public final class Looper {
* @param prefix A prefix to prepend to each line which is printed.
*/
public void dump(@NonNull Printer pw, @NonNull String prefix) {
+ if(mMessageRecorder != null) {
+ pw.println(mMessageRecorder.toString());
+ }
pw.println(prefix + toString());
mQueue.dump(pw, prefix + " ", null);
}
diff --git a/frameworks/base/core/java/android/os/MessageDispatchRecorder.java b/frameworks/base/core/java/android/os/MessageDispatchRecorder.java
new file mode 100644
index 0000000000..b0177d4296
--- /dev/null
+++ b/frameworks/base/core/java/android/os/MessageDispatchRecorder.java
@@ -0,0 +1,68 @@
+package android.os;
+
+import android.os.Message;
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+class MessageDispatchRecorder {
+ private final ArrayList<Recorder> mRecorders;
+ public MessageDispatchRecorder(){
+ mRecorders = new ArrayList<>();
+ }
+
+ public void beginRecord(Message msg){
+ Recorder recorder = new Recorder();
+ recorder.expectStartWhen = msg.getWhen();
+ recorder.realStatWhen = SystemClock.uptimeMillis();
+ recorder.descrip = "\""+msg.toString()+"\"";
+ synchronized(this){
+ mRecorders.add(recorder);
+ if(mRecorders.size() > 1000){
+ mRecorders.remove(0);
+ }
+ }
+ mRecorders.add(recorder);
+ if(mRecorders.size() > 1000){
+ mRecorders.remove(0);
+ }
+ }
+
+ public void endRecord(){
+ mRecorders.get(mRecorders.size()-1).realEndWhen = SystemClock.uptimeMillis();
+ }
+
+ @Override
+ public String toString() {
+
+ long now = SystemClock.uptimeMillis();
+ StringBuilder builder = new StringBuilder();
+ builder.append("now time is ");
+ builder.append(now);
+ builder.append("\n");
+ synchronized(this){
+ for (Recorder recorder:mRecorders){
+ if(recorder.realEndWhen > recorder.expectStartWhen) {
+ builder.append(String.format(Locale.CHINA,"when=%d realWhen=%d realEnd=%d duration=%d desc:%s",
+ recorder.expectStartWhen,recorder.realStatWhen,recorder.realEndWhen,
+ recorder.realEndWhen- recorder.realStatWhen,recorder.descrip));
+ } else {
+ builder.append(String.format(Locale.CHINA,"when=%d realWhen=%d realEnd=%d duration=%d desc:%s",
+ recorder.expectStartWhen,recorder.realStatWhen,recorder.realEndWhen,
+ 0,recorder.descrip));
+ }
+ builder.append("\n");
+ }
+ }
+
+ builder.append(String.format(Locale.CHINA,"there are %d history message\n",mRecorders.size()));
+ return builder.toString();
+ }
+ private static class Recorder{
+ long expectStartWhen;
+ long realStatWhen;
+ long realEndWhen;
+ String descrip;
+ }
+}
\ No newline at end of file
```
大体做法就是,记录主线程Looper的消息分发记录,当触发ANR(或者接收到 Signal 3)的时候,将消息分发记录打印到trace文件里面,辅助分析。
效果大概如下
{% asset_img record.png %}
注:上面的示范中,没有去移除分发记录,在实际的生产环境里面,需要为移除分发记录设定一定的条件,避免占用过多内存。]]></content>
<tags>
<tag>Android</tag>
<tag>ANR</tag>
</tags>
</entry>
<entry>
<title>Android Native Service 示例 第二篇</title>
<url>/2024/07/31/Android-native-service-sample-2/</url>
<content><![CDATA[在安卓里面实现一个 native 的系统服务,不仅可以通过定义 Interface ,还可以通过直接继承 BBinder 来实现。本篇文章即讲述如何通过继承 Bbinder 来实现一个系统服务,并且讲述如何在 native 上使用 binder 的 linktoDeath 机制
<!-- more -->
首先列出需要包含的头文件
```cpp
// CommonHeader.h
#ifndef _COMMON_HEADER_H_
#define _COMMON_HEADER_H_
#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <binder/MemoryBase.h>
#include <binder/MemoryHeapBase.h>
#include <binder/PermissionController.h>
#include <binder/ProcessInfoService.h>
#include <binder/IResultReceiver.h>
#include <utils/String8.h>
#include <utils/String16.h>
#include <log/log_main.h>
#include "Cmdid.h"
#ifdef LOG_TAG
#undef LOG_TAG
#endif
#define LOG_TAG "NativeService"
#define NATIVESERVICE_NAME "nativeservice"