1.Android Adb 源码分析(一)
2.论ååºç° MySQL Query Error
3.Wi-Fi p2p & ap å
±å
4.Android性能优化:定性和定位Android图形性能问题——以后台录屏进程为例
Android Adb 源码分析(一)
面对Android项目的调试困境,我们的团队在项目临近量产阶段,将userdebug版本切换为了user版本,并对selinux权限进行了调整。然而,这一转变却带来了大量的招工源码bug,日志文件在/data/logs/目录下,因为权限问题无法正常pull出来,导致问题定位变得异常困难。面对这一挑战,我们尝试了两种解决方案。
首先,我们尝试修改data目录的权限,使之成为system用户,以期绕过权限限制,然而数据目录下的logs文件仍保留了root权限,因此获取日志依然需要root权限,这并未解决问题。随后,我们找到了一个相对安全的解决办法——通过adb命令的后门机制,将获取root权限的命令修改为adb aaa.bbb.ccc.root。这一做法在一定程度上增加了后门的隐蔽性,避免了被窃取,同时对日常开发的影响也降至最低。
在解决这一问题的过程中,我们对Android ADB的相关知识有了更深入的理解。ADB是Android系统中用于调试的工具,它主要由三部分构成:adb client、adb service和adb daemon。其中,adb client运行于主机端,提供了命令接口;adb service作为一个后台进程,位于主机端;adb daemon则是运行于设备端(实际机器或模拟器)的守护进程。这三个组件共同构成了ADB工具的完整框架,且它们的代码主要来源于system/core/adb目录,用户可以在此目录下找到adb及adbd的源代码。
为了实现解决方案二,我们对adb的代码进行了修改,并通过Android SDK进行编译。具体步骤包括在Windows环境下编译生成adb.exe,以及在设备端编译adbd服务。需要注意的是,在进行编译前,需要先建立Android的编译环境。经过对ADB各部分关系及源代码结构的短线起爆指标公式源码梳理,我们对ADB有了更深入的理解。
在后续的开发过程中,我们将继续深入研究ADB代码,尤其是关于如何实现root权限的功能。如果大家觉得我们的分享有价值,欢迎关注我们的微信公众号“嵌入式Linux”,一起探索更多关于Android调试的技巧与知识。
论ååºç° MySQL Query Error
解å³æ¹æ¡å¦ä¸:
1. è¿å ¥ç®¡çmysqlçphpmyadmin
2. å¨å·¦åéä¸èªå·±çæ°æ®åº
3. å¨å³åå¾éä¸é误信æ¯ä¸çé£ä¸ªâwxpetdataâ表
4. æ»å¨å±å¹å°ä¸é¢ï¼æ个ä¸æèå(With selected:)ï¼éæ©âRepair tableâ
---------------------------------------------------------------------
æè å¯ä»¥è¿æ ·
wxpetdata被æ è®°æé®é¢ï¼éè¦ä¿®å¤ãäºæ¯èµ¶å¿«æ¢å¤åå²æ°æ®ï¼ä¸ç½æ¥æ¾åå ãæç»å°é®é¢è§£å³ã解å³æ¹æ³å¦ä¸ï¼
æ¾å°mysqlçå®è£ ç®å½çbin/myisamchkå·¥å ·ï¼å¨å½ä»¤è¡ä¸è¾å ¥ï¼
myisamchk -c -r ../data/dedecmsv4/dede_archives.MYI
ç¶åmyisamchk å·¥å ·ä¼å¸®å©ä½ æ¢å¤æ°æ®è¡¨çç´¢å¼ãéæ°å¯å¨mysqlï¼é®é¢è§£å³ã
é®é¢åæï¼
1ãé误产çåå ï¼æç½å说æ¯é¢ç¹æ¥è¯¢åæ´æ°dede_archives表é æçç´¢å¼é误ï¼å 为æç页é¢æ²¡æéæçæï¼èæ¯å¨æ页é¢ï¼å æ¤æ¯è¾åæè¿ç§è¯´æ³ãè¿æ说æ³ä¸ºæ¯MYSQLæ°æ®åºå 为æç§åå èåå°äºæåï¼å¦ï¼æ°æ®åºæå¡å¨çªåæ§çæçµãå¨æå¨æ°æ®åºè¡¨æä¾æå¡æ¶å¯¹è¡¨çåæ件è¿è¡æç§æä½é½æå¯è½å¯¼è´MYSQLæ°æ®åºè¡¨è¢«æåèæ æ³è¯»åæ°æ®ãæ»ä¹å°±æ¯å 为æäºä¸å¯æµçé®é¢é æ表çæåã
é®é¢çç¼å·ä¸º
2ãé®é¢è§£å³åæ³ã
å½ä½ è¯å¾ä¿®å¤ä¸ä¸ªè¢«ç ´åç表çé®é¢æ¶ï¼æä¸ç§ä¿®å¤ç±»åãå¦æä½ å¾å°ä¸ä¸ªé误信æ¯æåºä¸ä¸ªä¸´æ¶æ件ä¸è½å»ºç«ï¼å é¤ä¿¡æ¯ææåºçæ件并åè¯ä¸æ¬¡--è¿é常æ¯ä¸ä¸æ¬¡ä¿®å¤æä½éçä¸æ¥çã
è¿ä¸ç§ä¿®å¤æ¹æ³å¦ä¸æ示ï¼
% myisamchk --recover --quick /path/to/tblName
% myisamchk --recover /path/to/tblName
% myisamchk --safe-recover /path/to/tblName
第ä¸ç§æ¯æå¿«çï¼ç¨æ¥ä¿®å¤ææ®éçé®é¢ï¼èæåä¸ç§æ¯ææ ¢çï¼ç¨æ¥ä¿®å¤ä¸äºå ¶å®æ¹æ³æä¸è½ä¿®å¤çé®é¢ã
æ£æ¥åä¿®å¤MySQLæ°æ®æ件
å¦æä¸é¢çæ¹æ³æ æ³ä¿®å¤ä¸ä¸ªè¢«æåç表ï¼å¨ä½ æ¾å¼ä¹åï¼ä½ è¿å¯ä»¥è¯è¯ä¸é¢è¿ä¸¤ä¸ªæå·§ï¼
å¦æä½ æç表çç´¢å¼æ件(*.MYI)åçäºä¸å¯ä¿®å¤çé误ï¼çè³æ¯ä¸¢å¤±äºè¿ä¸ªæ件ï¼ä½ å¯ä»¥ä½¿ç¨æ°æ®æ件(*.MYD)åæ°æ®æ ¼å¼æ件(*.frm)éæ°çæå®ãé¦å å¶ä½ä¸ä¸ªæ°æ®æ件(tblName.MYD)çæ·è´ãéå¯ä½ çMySQLæå¡å¹¶è¿æ¥å°è¿ä¸ªæå¡ä¸ï¼ä½¿ç¨ä¸é¢çå½ä»¤å é¤è¡¨çå 容ï¼
mysql> DELETE FROM tblName;
å¨å é¤è¡¨çå 容çåæ¶ï¼ä¼å»ºç«ä¸ä¸ªæ°çç´¢å¼æ件ãéåºç»å½å¹¶éæ°å ³éæå¡ï¼ç¶åç¨ä½ åæä¿åçæ°æ®æ件(tblName.MYD)è¦çæ°ç(空)æ°æ®æ件ãæåï¼ä½¿ç¨myisamchkæ§è¡æ åçä¿®å¤(ä¸é¢ç第äºç§æ¹æ³)ï¼æ ¹æ®è¡¨çæ°æ®çå 容å表çæ ¼å¼æ件éæ°çæç´¢å¼æ°æ®ã
å¦æä½ ç表çæ ¼å¼æ件(tblName.frm)丢失äºæè æ¯åçäºä¸å¯ä¿®å¤çé误ï¼ä½æ¯ä½ æ¸ æ¥å¦ä½ä½¿ç¨ç¸åºçCREATE TABLEè¯å¥æ¥éæ°çæè¿å¼ 表ï¼ä½ å¯ä»¥éæ°çæä¸ä¸ªæ°ç.frmæ件并åä½ çæ°æ®æ件åç´¢å¼æ件(å¦æç´¢å¼æ件æé®é¢ï¼ä½¿ç¨ä¸é¢çæ¹æ³é建ä¸ä¸ªæ°ç)ä¸èµ·ä½¿ç¨ãé¦å å¶ä½ä¸ä¸ªæ°æ®åç´¢å¼æ件çæ·è´ï¼ç¶åå é¤åæ¥çæ件(å é¤æ°æ®ç®å½ä¸æå ³è¿ä¸ªè¡¨çææè®°å½)ã
å¯å¨MySQLæå¡å¹¶ä½¿ç¨å½åçCREATE TABLEæ件建ç«ä¸ä¸ªæ°ç表ãæ°ç.frmæ件åºè¯¥å¯ä»¥æ£å¸¸å·¥ä½äºï¼ä½æ¯æå¥½ä½ è¿æ¯æ§è¡ä¸ä¸æ åçä¿®å¤(ä¸é¢ç第äºç§æ¹æ³)ã
3ãmyisamchkå·¥å ·ä»ç»ï¼è§mysqlçå®æ¹æåï¼
å¯ä»¥ä½¿ç¨myisamchkå®ç¨ç¨åºæ¥è·å¾æå ³æ°æ®åºè¡¨çä¿¡æ¯ææ£æ¥ãä¿®å¤ãä¼åä»ä»¬ãmyisamchkéç¨MyISAM表(对åº.MYIå.MYDæ件ç表)ã
è°ç¨myisamchkçæ¹æ³ï¼
shell> myisamchk [options] tbl_name ...
optionsæå®ä½ æ³è®©myisamchkåä»ä¹ãå¨åé¢æè¿°å®ä»¬ãè¿å¯ä»¥éè¿è°ç¨myisamchk --helpå¾å°é项å表ã
tbl_nameæ¯ä½ æ³è¦æ£æ¥æä¿®å¤çæ°æ®åºè¡¨ãå¦æä½ ä¸å¨æ°æ®åºç®å½çæå¤è¿è¡myisamchkï¼ä½ å¿ é¡»æå®æ°æ®åºç®å½çè·¯å¾ï¼å 为myisamchkä¸ç¥éä½ çæ°æ®åºä½äºåªå¿ãå®é ä¸ï¼myisamchkä¸å¨ä¹ä½ æ£å¨æä½çæ件æ¯å¦ä½äºä¸ä¸ªæ°æ®åºç®å½ï¼ä½ å¯ä»¥å°å¯¹åºäºæ°æ®åºè¡¨çæ件æ·è´å°å«å¤å¹¶ä¸å¨é£éæ§è¡æ¢å¤æä½ã
å¦æä½ æ¿æï¼å¯ä»¥ç¨myisamchkå½ä»¤è¡å½åå 个表ãè¿å¯ä»¥éè¿å½åç´¢å¼æ件(ç¨â .MYIâåç¼)æ¥æå®ä¸ä¸ªè¡¨ãå®å è®¸ä½ éè¿ä½¿ç¨æ¨¡å¼â*.MYIâæå®å¨ä¸ä¸ªç®å½ææç表ãä¾å¦ï¼å¦æä½ å¨æ°æ®åºç®å½ï¼å¯ä»¥è¿æ ·å¨ç®å½ä¸æ£æ¥ææçMyISAM表ï¼
shell> myisamchk *.MYI
å¦æä½ ä¸å¨æ°æ®åºç®å½ä¸ï¼å¯éè¿æå®å°ç®å½çè·¯å¾æ£æ¥ææå¨é£éç表ï¼
shell> myisamchk /path/to/database_dir/*.MYI
ä½ çè³å¯ä»¥éè¿ä¸ºMySQLæ°æ®ç®å½çè·¯å¾æå®ä¸ä¸ªéé 符æ¥æ£æ¥ææçæ°æ®åºä¸çææ表ï¼
shell> myisamchk /path/to/datadir/*/*.MYI
æ¨èçå¿«éæ£æ¥ææMyISAM表çæ¹å¼æ¯ï¼
shell> myisamchk --silent --fast /path/to/datadir/*/*.MYI
å¦æä½ æ³è¦æ£æ¥ææMyISAM表并修å¤ä»»ä½ç ´åç表ï¼å¯ä»¥ä½¿ç¨ä¸é¢çå½ä»¤ï¼
shell> myisamchk --silent --force --fast --update-state \
-O key_buffer=M -O sort_buffer=M \
-O read_buffer=1M -O write_buffer=1M \
/path/to/datadir/*/*.MYI
该å½ä»¤åå®ä½ æ大äºMBçèªç±å åãå ³äºç¨myisamchkåé å åç详ç»ä¿¡æ¯ï¼åè§5.9.5.5èï¼âmyisamchkå å使ç¨âã
å½ä½ è¿è¡myisamchkæ¶ï¼å¿ 须确ä¿å ¶å®ç¨åºä¸ä½¿ç¨è¡¨ãå¦åï¼å½ä½ è¿è¡myisamchkæ¶ï¼ä¼æ¾ç¤ºä¸é¢çé误æ¶æ¯ï¼
warning: clients are using or haven't closed the table properly
è¿è¯´æä½ æ£å°è¯æ£æ¥æ£è¢«å¦ä¸ä¸ªè¿æ²¡æå ³éæ件æå·²ç»ç»æ¢è没ææ£ç¡®å°å ³éæ件çç¨åº(ä¾å¦mysqldæå¡å¨)æ´æ°ç表ã
å¦æmysqldæ£å¨è¿è¡ï¼ä½ å¿ é¡»éè¿FLUSH TABLES强å¶æ¸ 空ä»ç¶å¨å åä¸çä»»ä½è¡¨ä¿®æ¹ãå½ä½ è¿è¡myisamchkæ¶ï¼å¿ 须确ä¿å ¶å®ç¨åºä¸ä½¿ç¨è¡¨ãé¿å 该é®é¢çæ容æçæ¹æ³æ¯ä½¿ç¨CHECK TABLEèä¸ç¨myisamchkæ¥æ£æ¥è¡¨ã
Wi-Fi p2p & ap å ±å
nlæ¥å£
ç¼è¾
å é¤
WiFiçå ±å模å¼ï¼
ap mode éç¨åºç¨å¨æ 线å±åç½æå设å¤ï¼å³å®¢æ·ç«¯ï¼çå å ¥ï¼å³ç½ç»ä¸è¡ãå®æä¾ä»¥æ 线æ¹å¼ç»å»ºæ 线å±åç½WLANï¼ç¸å½é WLANçä¸å¿è®¾å¤ã
station modeå³å·¥ä½ç«æ¨¡å¼ï¼å¯ä»¥ç解为æ个ç½æ ¼ä¸çä¸ä¸ªå·¥ä½ç«å³å®¢æ·ç«¯ãé£å½ä¸ä¸ªWIFIè¯çæä¾è¿ä¸ªåè½æ¶ï¼å®å°±å¯ä»¥è¿å°å¦å¤çä¸ä¸ªç½ç»å½ä¸ï¼å¦å®¶ç¨è·¯ç±å¨ãé常ç¨äºæä¾ç½ç»çæ°æ®ä¸è¡æå¡
p2p modeä¹ä¸ºWi-Fi Direct
Wi-Fi Direct æ¯ä¸ç§ç¹å¯¹ç¹è¿æ¥ææ¯ï¼å®å¯ä»¥å¨ä¸¤å° station ä¹é´ç´æ¥å»ºç« tcp/ip é¾æ¥ï¼å¹¶ä¸éè¦APçåä¸ï¼å ¶ä¸ä¸å°stationä¼èµ·å°ä¼ ç»æä¹ä¸çAPçä½ç¨ï¼ç§°ä¸ºGroup Owner(GO),å¦å¤ä¸å°stationå称为Group Client(GC)ï¼åè¿æ¥APä¸æ ·è¿æ¥å°GOãGOåGCä¸ä» å¯ä»¥æ¯ä¸å¯¹ä¸ï¼ä¹å¯ä»¥æ¯ä¸å¯¹å¤ï¼æ¯å¦ï¼ä¸å°GOå¯ä»¥åæ¶è¿æ¥çå¤å°GC
wpa_supplicant æ¯ä¸ä¸ªè¿æ¥ãé ç½® WIFI çå·¥å ·ï¼å®ä¸»è¦å å« wpa_supplicant ä¸ wpa_cli 两个ç¨åº. å¯ä»¥éè¿ wpa_cli æ¥è¿è¡ WIFI çé ç½®ä¸è¿æ¥,åæè¦ä¿è¯ wpa_supplicant æ£å¸¸å¯å¨ã
wpa_supplicantæ¬æ¯å¼æºé¡¹ç®æºç ï¼è¢«è°·æä¿®æ¹åå å ¥Android移å¨å¹³å°ï¼å®ä¸»è¦æ¯ç¨æ¥æ¯æWEPï¼WPA/WPA2åWAPIæ 线åè®®åå å¯è®¤è¯çï¼èå®é ä¸çå·¥ä½å 容æ¯éè¿socketï¼ä¸ç®¡æ¯wpa_supplicantä¸ä¸å±è¿æ¯wpa_supplicantä¸é©±å¨é½éç¨socketé讯ï¼ä¸é©±å¨äº¤äºä¸æ¥æ°æ®ç»ç¨æ·ï¼èç¨æ·å¯ä»¥éè¿socketåéå½ä»¤ç»wpa_supplicantè°å¨é©±å¨æ¥å¯¹WiFiè¯çæä½ã ç®åç说ï¼wpa_supplicantå°±æ¯WiFi驱å¨åç¨æ·çä¸è½¬ç«å¤å 对åè®®åå å¯è®¤è¯çæ¯æã
ç®åå¯ä»¥ä½¿ç¨wireless-tools æwpa_supplicantå·¥å ·æ¥é ç½®æ 线ç½ç»ã请记ä½éè¦çä¸ç¹æ¯ï¼å¯¹æ 线ç½ç»çé ç½®æ¯å ¨å±æ§çï¼èééå¯¹å ·ä½çæ¥å£ã
wpa_supplicantæ¯ä¸ä¸ªè¾å¥½çéæ©ï¼ä½ç¼ºç¹æ¯å®ä¸æ¯æææç驱å¨ã请æµè§wpa_supplicantç½ç«è·å¾å®ææ¯æç驱å¨å表ãå¦å¤ï¼wpa_supplicantç®ååªè½è¿æ¥å°é£äºä½ å·²ç»é 置好ESSIDçæ 线ç½ç»ãwireless-toolsæ¯æå ä¹ææçæ 线ç½å¡å驱å¨ï¼ä½å®ä¸è½è¿æ¥å°é£äºåªæ¯æWPAçAPã
ç»è¿ç¼è¯åç wpa_supplicant æºç¨åºå¯ä»¥çå°ä¸¤ä¸ªä¸»è¦çå¯æ§è¡å·¥å ·ï¼ wpa_supplicant å wpa_cli ã wpa_supplicant æ¯æ ¸å¿ç¨åºï¼å®å wpa_cli çå ³ç³»å°±æ¯æå¡å客æ·ç«¯çå ³ç³»ï¼åå°è¿è¡wpa_supplicantï¼ä½¿ç¨wpa_cliæ¥æç´¢ã设置ãåè¿æ¥ç½ç»ã
1ï¼éè¿adbå½ä»¤è¡ï¼å¯ä»¥ç´æ¥æå¼supplicantï¼ä»èè¿è¡wpa_cliï¼å¯ä»¥è§£å³å®¢æ·æ²¡ææ¾ç¤ºå±èæ æ³æä½WIFIçé®é¢ï¼è¿å¯ä»¥é¿å UIçé®é¢å¸¦å°driverãè¿ä¸æ¥æ¥è¯´ï¼å¯ä»¥ç¨å¨å¾å¤æ²¡æé®çè¾å ¥åLCDè¾åºçå®å ç»ç«¯ 产åçæä½ä¸ã
wpa_supplicantå å«ä¸¤ä¸ªä¸»è¦çå¯æ§è¡å·¥å ·ï¼ wpa_supplicant å wpa_cli ãwpa_supplicant æ¯æ ¸å¿ç¨åºï¼å®å wpa_cli çå ³ç³»å°±æ¯æå¡å客æ·ç«¯çå ³ç³»ï¼åå°è¿è¡ wpa_supplicantï¼ä½¿ç¨ wpa_cli æ¥æç´¢ã设置ãåè¿æ¥ç½ç»ã
åè:
/index.php/OMAP_Wireless_Connectivity_NLCP_WiFi_Direct_Configuration_Scripts
Android性能优化:定性和定位Android图形性能问题——以后台录屏进程为例
简介发现、定性与定位
总结
跟不上旋律节奏的VSYNC
严重异常耗时的dequeueBuffer
VirtualDisplay合成耗时
结论
FPS
初步定位问题
定性问题
定位问题
成果展示
参考
简介本文记录一次Android图形性能问题的分析过程——发现、定性和定位图形性能问题,以及探讨的性能优化方案。
环境:Android Q + MTK + ARM Mali-G。
所分析的性能问题(下称case):打开录屏应用并启动后台录屏,滑动前台应用(滑屏)。性能表现差:CPU、GPU负载显著升高、掉帧、用户明显卡顿感,帧率不足帧,帧渲染、合成耗时急剧飙升(渲染耗时平均为ms左右)。
经过优化后,相同环境和条件下,渲染帧率稳定在帧(提升一倍),渲染耗时平均为8.ms左右(为优化前的不到三分之一的消耗)。
关键词 Keywords: Screen Recording; Frame rate; FPS; GPU utilization; Jank; MediaProjection; VirtualDisplay; MediaCodec; Perfetto; Inferno; Surface; SurfaceTexture; VSYNC; SurfaceFlinger; HWC; Hardware composer; GPU; OpenGL;
发现、定性与定位FPS计算FPS的方法和工具 Android框架层通过hwui配合底层完成渲染。该框架本身提供了逐帧渲染分段耗时记录。通过dumpsys gfxinfo可以获取。
io.microshow.screenrecorder/io.microshow.screenrecorder.activity.MainActivity/android.view.ViewRootImpl@6b9b8a9?(visibility=0)DrawPrepare?Process?Execute3...................1................使用工具统计帧率与平均耗时(同时打印GPU负载),在开启后台录屏的情况下滑动屏幕,平均渲染耗时高达~ms,超出.ms一倍,导致帧率仅帧,显著低于帧。
Average?elapsed?.?msFPS:??│?9.?0.?.?2.#?GPU负载?LOADING?BLOCKING?IDLE?0?#?case的对比——未开启后台录屏Average?elapsed?9.?msFPS:??│?1.?0.?5.?1.通过gfx柱状图直观感受性能数据 直观地感受图形渲染性能,除了帧率感受、触控延时外,还可以通过将gfxinfo的分段耗时通过柱状图展示在屏幕上。
这是case性能问题的gfxinfo柱状图,可以看到红柱和绿柱都非常高,远远超越了流畅标准。其中,绿柱异常放大表明两个Vsync之间耗时显著增长,js实现轮播图源码红柱异常放大表明应用层应用加速使用的DisplayLists大量增长、或图形层使用GLES调用GPU耗时显著增多导致的GPU执行绘制指令耗时变长。
初步定位问题本节记录初步的分析思路和定位过程。首先我们完成实验(启停后台录屏并滑动屏幕触发渲染)、观测以及记录,拿到了后台录屏启停情况下的FPS、分阶段耗时以及GPU负载(相关数据位于FPS小节)。
开发的工具输出的统计数据计算结果非常直观,一眼可见,后台录屏为Draw阶段带来额外的~8倍或~8ms耗时,给Process阶段带来额外的~2倍或~ms耗时。帧率从帧坠落到~帧。
耗时分析 可以看到,主要的额外耗时来自Draw和Process。接下来重点围绕着两part定位问题问题。
StageDescriptionCompDraw创建DisplayLists的耗时。Android的View如果支持硬件加速,绘制工作均通过DisplayLists由GPU绘制,可以处理为onDraw的耗时额外~8ms或~8倍Prepare准备没有额外耗时ProcessDisplayLists执行耗时。即硬件加速机制下提交给GPU绘制的工作耗时额外~ms或~2倍ExecuteFramebuffer前后缓冲区flip动作的耗时,上屏耗时额外不到~1msHz下,上述4个步骤合计耗时小于.ms为正常情况。case为~ms。主要增量来自Draw和Process。
经过上述初步分析、观测后,接下来的分析可以围绕Draw和Process开展。由于Android Draw部分涉及较广,包含App 渲染线程(DisplayLists)、UI线程(onDraw方法创建DisplayLists),以及图形栈耗时如SurfaceFlinger、RenderEngine等都可能增加Draw耗时。
这里一个技巧可以初步判断耗时来自App进程(渲染线程和UI线程)还是来自图形栈。如果能判断耗时来自App或图形栈,那么可以缩小分析范围、减少分析工作量。上述四大阶段的耗时统计分类比较宽,实际上还有更详细的分阶段耗时,它呈现在前文描述过的gfx统计信息柱状图上。gfx柱状图会以蓝色(RGB(,,))呈现onDraw方法创建和更新DisplayLists的耗时。如果case与正常情况对比后,这部分耗时(蓝柱大小对比)差异很小,即可说明额外的Draw耗时不是来自App的,极可能来自图形栈。剑侠情缘外挂源码Besides,结合过度绘制分析,判断case与正常情况下是否有更多的额外绘制次数可以协同判断。
——根据上述指导思想,排查出了case的额外Draw耗时与App onDraw无关,多出来的DisplayLists来自App以外的进程,可能是图形栈如SurfaceFlinger。
定性问题本小节介绍问题追踪过程,通过一些方法定位到各阶段的耗时原因,并定性地得出case性能问题的性质。从本小节开始,围绕Perfetto进行分析。这里贴出perfetto的总览,我将关键的信息排序到顶部。前四行分别为SF负责图形的线程、提交到GPU等待完成的工作、Vsync-App、Vsync-sf,最后两行为case中出现卡顿掉帧的App的主线程(UI)和渲染线程(RenderThread)。
跟不上旋律节奏的VSYNC容易看到,Vsync-sf非常不规律。Vsync-sf是触发SurfaceFlinger一次合成工作的基于Hardware VSYNC虚拟出来的一个信号。它相对于真实硬件信号(HW_VSYNC)一个规律的偏移(在case设备上,Vsync-app与Vsync-sf都被配置为8.3ms,即硬件VSYNC到达后,虚拟的Vsync-app和Vsync-sf延时8.3ms后发出,分别触发App绘制、SurfaceFlinger合成。
而case的Vsync-sf交错、残次、不齐、无规律,显然工况不佳。它将导致SurfaceFlinger不能按照预期的时间间隔将合成的帧提交到Framebuffer(经过Flip后,被提交的Framebuffer将上屏成为显示器的下一帧图像),出现掉帧/丢帧。
As we can see,case的VSYNC-sf出现严重的漂移(见图,第二行的VSYNC-sf残次不齐、跟不上规律、难看且混乱),这导致了丢帧。(但VSYNC-sf的失控仅表示与丢帧的相关性,并不直接表明因果性。顺风车 小程序 源码)
VSYNC-sf为什么会出现偏差? 出于功耗的考虑,VSYNC-sf合VSYNC-app并不是一定会触发的。如果app或sf并没有更新画面的需求,那么死板固定地调度它们进行绘制和合成是不必的。编程上,负责触发VSYNC-sf和VSYNC-app的两个EventThread会在requestNextVsync调用后才会将下一个VSYNC-sf或VSYNC-app发出。因此,当(各自EventThread的)requestNextVsync没有调用时,VSYNC-app和VSYNC-sf也就出现漂移。BufferQueueLayer::onFrameAvailable会在应用提交后调用,该方法通过调用SF的signalLayerUpdate触发产生下一个VSYNC-sf。
换而言之,出于功耗,或别的什么原因(比如耗时导致的延期,人家是线程实现的消息队列),SurfaceFlinger的SFEventThread有可能不调用requestNextVsync,这将导致Vsync-sf在窗口期内短暂消失——但是也不会出现参差不齐的情况。结合case的VSYNC信号报告来看,VSYNC-sf信号异常切实地提示了性能问题——它的不规律现象表明前后Vsync之间有异常耗时,而非低功耗机制被激活或无屏幕刷新(case性能问题复现时一直在滑前台应用的屏,它每ms都有画面更新的需求)。
VSYNC-sf虽然出现了偏差,但是它与卡顿问题仅有相关性(或者说它是性能问题的结果),并非因果关系。猜测是其他卡顿问题导致了SF延缓了对VSYNC的request,导致其信号出现漂移。VSYNC-sf信号偏差实质上指导意义重大,因为它能提示我们,问题发生在比App更底层的地方(前文分析的结论),且比SurfaceFlinger提交到Framebuffer更上层的位置(VSYNC-sf用于触发合成,合成完成后提交到屏幕双缓冲区)。
这样,将case性能问题的上下界都确定了,问题分析范围从原先的整个图形栈,有效的缩小到了SurfaceFlinger渲染和合成阶段了。
严重异常耗时的dequeueBuffer通读Perfetto,可以看到,出了难看的Vsync-sf以外,还可以看到刺眼的超长耗时的draw(App UI线程)以及耗时变态长的dequeueBuffer(App 渲染线程)调用。相对于正常情况,perfetto报告提示的case的draw方法成倍增长的耗时非常容易被误认为耗时“居然来自一开始就排除掉的App进程",这与前文提出的”问题范围“是不能自洽的——它们是相反的结论,肯定哪里不对。仔细分析才能发现,draw方法确实是消耗了更多墙上时间(但是不意味着消耗了更多CPU时间,因为等待过程是sleep的),但是draw方法是因为等待渲染线程的dequeueBuffer造成的耗时,而dequeueBuffer的严重异常耗时却是被底层的图形栈拖累的。
我们看到,draw严重耗时,渲染线程dequeueBuffer消耗掉~ms的时间。As we all known,Android的Graphics buffer是生产者消费者模型,当作为消费者的SF来不及处理buffer并释放,渲染线程也就需要额外耗时等待buffer就绪。上面还有一段"Waiting GPU Completion"的trace没有贴上来(下图),这段耗时比不开启后台录屏的case下高得多(~3ms对比~ms),说明了一定的GPU性能问题或SF的性能问题,甚至有可能是Display有问题(HWC release耗时过长也会导致SF释放buf、生产者渲染线程dequeueBuffer额外等待)。
这里的机制比较复杂,不熟悉底层Graphics buffer的流水线模型就不好理解。In one world, dequeueBuffer申请的buffer不是凭空new出来的,而是在App-SurfaceFlinger-Framebuffer这一流水线中循环使用的。流水线中的buffer不是无限的,而是有穷的几个。当底层的伙计,如SF和HWC,使用了buffer但是没有来得及释放时(它们的工作没做完之前不会释放buffer),流水线(可以理解成头尾相接的单向队列(ring buffer))没有可用的buffer,此时dequeueBuffer就不得不进入等待,出现耗时看上去很长的问题。实际上,dequeueBuffer耗时的唯一原因几乎仅仅只有一个:底层消费太慢了,流水线没有剩余buffer,因此需要等待。
这个模型抽象理解非常简单。下图,右边消费者是底层图形栈——它每消费完一个buffer就会释放掉,每释放一个buffer应用层能用的buffer就加1。左边生产者是App渲染线程——它调用dequeueBuffer申请一个buffer以将它的画面绘制到这个buffer上。buffer送入BufferQueue后由右边的消费者(图形栈)进行消费(合成、上屏显示),然后释放buffer。当图形栈来不及release buffer时,dequeueBuffer的调用者(App渲染线程)将由于无可用buffer,就必须挂起等待了,在perfetto上就留下长长的一段”耗时“(实际上是墙上时间,大部分都没有占用CPU)。
以上,这就是为什么说App渲染线程dequeueBuffer严重耗时中的耗时为什么要打引号,为什么要说是被图形层拖累了。
下图可以看到,刨去dequeueBuffer的严重异常耗时,执行渲染的部分耗时相对于正常的case几乎没有差异,这可以断言渲染线程的惨烈耗时主要就是被dequeueBuffer浪费了。
从GPU Completion来看,此时GPU正在为SF工作,因为在图中看到(不好意思没有截全,下图你是看不出来的),dequeueBuffer总是在SF的GPU Completion结束之后结束的,这就表明SF正在通过GPU消费buffer(调用GPU进行合成后提交,然后标记buffer允许被渲染线程dequeue)。dequeueBuffer获取到就绪的buffer此时此刻取决于SF的消费能力——因为case中它是短板。(当然图形层的buffer可用不止SurfaceFlinger需要释放,因为SF释放后buffer实质上流转到更底层的HWC,等它将Buffer提交到屏幕后才会释放,这里释放后才能给App再次使用(上面哪个模型图把SF和HWC合并为流水线的图形层buffer消费者)。
从perfetto报告看HWC release非常及时、余量充足,SF的GPU Completion则较紧密地接着dequeueBuffer返回,基本断言是SF太慢了——排除HWC的责任。(下图看不出来,当时没有截图到HWC的release情况。)
到这里,除了再次确认排除了前台App的问题外,还可以断言问题来自SurfaceFlinger过分耗时。此外将问题范围的下界从整个SF合成流程(上文的Vsync-sf)缩小到了排除HWC的范围。
结论:渲染耗时一切正常,问题出现在SF消费buffer(合成图形)失速了,导致没有可用的buffer供渲染线程使用。从下图的SF的工况(第三列)来看,情况确实如此。
既然一口咬定是SF的锅,那就瞧瞧SF。先看SF的INVALIDATE,这没啥好看的,异常case和正常case都是~2.5ms。主要看refresh,正常case ~6.8ms,异常case ~.8ms。refresh包含SF的合成四件套,包括rebuildLayerStack、CalcuateWorkingSet、Prepare、doComposition。Perfetto报告直接表明,case的后台录屏导致的额外一次合成和配套工作是主要的耗时增量。
之所以会执行两次合成,是因为后台录屏工具编程上通过Android SDK提供的MediaProjection配合VirtualDisplay实现一个虚拟的镜像的屏幕。SurfaceFlinger会将画面输送一份到这个虚拟的Display以实现屏幕图像传送到录屏工具,虚拟的屏幕要求额外的一次合成。从上图可以直接得出结论,case带来的额外工作消耗就是对该录屏用的VirtualDisplay的合成工作(doComposition)带来的。
VirtualDisplay合成耗时由于问题范围已经缩小到了很小的一个范围,在SurfaceFlinger的Refresh过程中,case相对正常应用有巨大的差异耗时,几乎完全来自于对VirtualDisplay的合成耗时(doComposition)。同时也可以看到,两次合成(一次是设备的物理屏幕,一次是case的后台录屏工具创建的虚拟屏幕)中,虚拟屏幕的耗时远远高于物理屏幕(4倍以上)。
通过查看ATRACE的tag(上图,Perfetto中SurfaceFlinger中主线程的各个trace point都是用ATRACE打的tag),结合dumpsys SurfaceFlinger,能直接看到的线索是:
虚拟屏显著耗时,且合成工作通过GLES调用GPU完成
物理屏合成耗时很小,它通过HWC合成
结合图中提示的trace tag、耗时,可以得出结论,使用GPU合成的虚拟屏中因GPU合成耗时很长,导致它显著高于物理屏HWC合成耗时。如果GPU合成能够和HWC合成一样快,或者干脆让虚拟屏也使用HWC合成,那么可以预期SurfaceFlinger的合成工作的消耗将显著降低。
结论本小节综合上述三个小节的分析,对节”定性问题“下一个结论。
耗时的本质已经被看透,录屏工具申请创建的VirtualDisplay没有通过HWC进行合成,而是通过GPU进行合成,它耗时很长导致界面卡顿。In one word,case使用的VirtualDisplay的合成方式不够高效。
HWC是Hardward Composer。它接收图形数据,类似于往桌面(真的桌面,不是电脑和手机的桌面)上面叠放照片和纸张——即合成过程。这个工作能将界面上几个窗口叠加在一起后送到屏幕上显示。通过GLES调动GPU也能干这活,不过HWC执行合成的动作是纯硬件的——它很快,比GPU快几倍。
定位问题前面虽然定性了问题原因是合成方式不够高效,但是没有得出其中的原理——为什么虚拟屏不使用高效的HWC进行合成。本节通过介绍HWC的原理、SurfaceFlinger控制合成方式、虚拟屏Surface特性等来介绍图形栈中合成方式的处理模式。掌握了相关管理后,探讨一些尽量通用的共性的解决方案实现性能优化。最后着重介绍多套优化方案中的一种直面根本原因的解决方法——MediaCodec.MediaFormat创建的支持HWC合成的Surface方案。
SurfaceFlinger如何决定使用HWC还是GPU合成? SurfaceFlinger合成主要可以依靠两条路径。其中之一是”纯硬“的HWC合成(在dumpsys SurfaceFlinger中可以看到Composition type为DEVICE),另一个是通过OpenGL让GPU进行合成(Composition type为CLIENT)。
除非是功耗上的设计,否则SurfaceFlinger总是会优先检查本次合成是否支持使用HWC。编程上,在合成阶段之一的prepare过程中,SurfaceFlinger通过prepareFrame在RenderSurface与Hardware Composer(即HWC)的HIDL服务通信,完成hwc layer的创建。但是,layer能够成功创建不意味着一定支持HWC合成。SurfaceFlinger通过getChangedCompositionTypes向HWC查询不支持HWC合成的Layer。该方法返回的layer如果被标记为CLIENT合成,那么这部分Layer无法由HWC进行合成,而只能通过GPU进行合成——case的VirtualDisplay就是这个情况。
部分layer可能不能由HWC合成的原因(除功耗策略、其他软件策略外):
HWC layer达到上限 Hardware Composer支持的layer数量是有限的。查阅公开资料可知,HWC合成动作属于硬件提供的能力,它们的合成能力受到硬件本身的限制。Google官方资料对Android设备的要求是,HWC最少应该支持4个Layer,分别用于一个常规页面上最常见的4个层:壁纸、状态栏、导航栏和应用窗口。 在case设备中,经过测试,该平台的HWC最多支持7个能进行HWC合成的layer,从第8个layer开始,完完全全只能使用CLIENT合成亦即SurfaceFlinger调用RenderEngine通过OpenGL调动GPU进行合成。 正是由于HWC合成layer有上限,因此在弹出多个弹窗、叠加过于复杂时,即使界面简单也有可能出现比较明显的卡顿。
VirtualDisplay的Surface格式不受HWC支持 HWC的硬件合成能力对buffer(Surface封装)内保存的图像的格式有要求。比如,HWC不能处理缩放,仅支持一部分的格式,大多数都还有其他因素会导致不支持,如旋转、部分Alpha等等。In one word,图像格式的数量是远远多于HWC支持的类型数的。当HWC碰到不支持合成的Surface时,就会在前文提过的getChangedCompositionTypes中通知SurfaceFlinger,由SurfaceFlinger转为使用GPU合成。
结合上述几种情况,设计实验验证。其中通过在物理屏上弹窗来增加Layer以获取HWC Layer上限。确认case无法使用HWC合成不是Layer上限导致的问题后,通过对比来验证Surface格式问题。Surface是对native层的buffer的封装,其类型广泛、实现复杂,一个一个试是不现实的。通过对比性能强劲的类似实现可以一探究竟。Android adb提供一个出厂自带的录屏命令screenrecord、用于测试双屏显示功能的虚拟辅助屏幕(开发者模式-模拟辅助屏)、著名远程窥屏工具scrcpy等三个工具是一系列重要参考。
经过测试,screenrecord和scrcpy创建的VirtualDisplay支持HWC合成——这是优化目标。首先看看它们的实现。
编程上,虚拟辅助屏幕采用了与case一模一样的实现——通过创建VirtualDisplay让图形层额外合成一次屏幕到该虚拟屏幕中。虚拟屏幕本质上将画面发送给录屏功能实现,而非进行显示来完成录屏。
通读screenrecord源码,逻辑上,它与虚拟辅助屏、case录屏应用是相同的——VirtualDisplay录屏。但是编程上略有差异:
screenrecord直接通过binder与SurfaceFlinger通信,获取了raw VirtualDisplay,而