11:38 am
Friday, 10 February 2023 (HKT)
Time in Kowloon
- 布景
-
观测
- 1. trace表现UI制作操作严重耗时
- 2. 排查measure和layout慢的原因:可疑的屡次binder
- 3. binder:在哪、谁为、为何频频调用
- 4. binder:频频调用的详细定位
- 定论
- 计划
- 参阅
布景
某轮测验发现,咱们的设备运转一个第三方的App时,卡顿感十分明显:
- 界面加载很慢,菊花转半响
- 滑屏极度不跟手,目测观感帧率低于15
- 比照机(竞品)也会略微一点卡,但是好很多,根本不会有很大感觉的卡顿
能够初步断定咱们的设备存在功用问题,亟需优化,拉平到竞品水准。
终究发现,这个问题实际上是运用自身奇怪的完成(getResources()的重载),加上Binder过度调用(沉重的Binder耗时)导致的。
本文做记载和共享。
其间比照机装备、Android版别均与本机不同,不做变量参阅。
观测
由于这个心爱的App是第三方的App(运用市场下载的),咱们没有源码,只能从系统端去干涉。先抓一份trace。
1. trace表现UI制作操作严重耗时
trace一抓一看,明显App主线程现已陷入困境。能够看到:
- CPU运用率并不高
- 主线程几乎彻底在履行Traversal作业(mersure和layout)
- measure和layout极度耗时,明显达不到合理的帧率要求(甚至连PPT帧率都赶不上)
能够看到,这份trace标明App的整个measure和layout作业存在整体性的不合理耗时。但并不能精确提示细节,也不能看出问题部分。能够肯定,耗时作业坐落App层(不是指耗时原因也来自App)。
2. 排查measure和layout慢的原因:可疑的屡次binder
上面能够承认制作缓慢形成耗时。但是一来App不是自己的,二来这么杂乱的调用,经过剖析调用、跟代码来定位慢办法、慢路径明显满足低效。
定位到Traversal,计算一下Traversal各部分的耗时占比,能够大致定位出耗时部分可能是什么事务的:
能够看到,traversal意外地包括了数量巨大的binder调用,它占有总耗时的80%+,使得运用层绘图超出生命线10倍以上:
- 这次doFrame->travesal耗时挨近200ms,归于”无法运用的垃圾”等级,不是功用问题而是毛病
- binder调用(binder transaction)次数很多,在几毫秒的时刻里(预期的一次运用层绘图时刻)进行了194次IPC
- binder耗时占比很高:83%左右
- 还有一个ioctl调用次数也很多、很耗时;由于binder驱动调用talkWithDriver()需求运用ioctl,因而这儿初步判断ioctl是binder IPC的伴生,无碍
生命线:关于60Hz的屏幕,生命线为16ms左右。但是16ms为图形栈全链路的极限时刻,留给运用层的时刻更低
能够承认,过多的binder调用导致了这个恼火的功用问题。
3. binder:在哪、谁为、为何频频调用
通常运用(和运用集成的库),出于一定的意图,会经过IBinder、AIDL、封装组件(如startService)、直接调用驱动节点(talkWithDriver)等方式来进行一次Binder IPC。
功用问题中,与Binder IPC相关的,最常见的首要如下:
- 频频调用Binder
- 要害、灵敏、严重的方位调用Binder
- Binder对端响应太慢,对端繁忙
- Binder传递的数据太大
- Binder客户端线程数超限(建议请求的线程满)
- Binder服务端线程数超限(处理请求的线程满)
关于Binder传递数据太大、线程数导致的功用问题,由于运用不是自己的(欠好干涉、不关注),且比照机卡顿不那么明显(能够粗略排除),因而不太值得去看。(另外咱们是在滑屏的时分卡的,主线程UIHandler也做不到并发宣布Binder IPC)
这儿仍是展现一下怎么剖析。下列指令能够提供一些关于binder状况、traction状况、传递数据大小等内容:
cat /sys/kernel/debug/binder/failed_transaction_log
cat /sys/kernel/debug/binder/transaction_log
cat /sys/kernel/debug/binder/transactions
相同的,咱们欠好关注运用为何调用binder(由于没有App的代码,最近也忙的不想逆向它;但实际上终究咱们知道了为何调用),也很明显是在哪调用的(在App UI线程 performTraversal时调用的),因而先来看看这群IPC的对端是谁。
trace一看,binder调用的确很多(画蓝紫色线部分都是binder;本是细线,溢满则刚):
上图binder调用很多,其实很多是同一品种,各IPC都终究归归于一类Binder。分类看,数量巨大、占比最高的两类binder(称为榜首部分binder和第二部分binder)是值得讨论的首要耗时部分。
首要,剖析榜首部分binder的对端。跟踪发现榜首部分binder“飞”往SurafceFlinger,耗时较短,次数合理,评价正常,不再跟进,不贴图展现。
第二部分binder,从次数、耗时来看,的确可疑。它从App进程“飞”往System_server(Framework服务层):
4. binder:频频调用的详细定位
功用剖析的其间一个要害方向是找到慢办法、慢路径。上面一步现已表现了,慢是由于App在灵敏且要害的方位调用了Binder,这个binder的对端是Framework。
从系统侧剖析这个binder的功用,难以像App那样轻松定位——由于App里边有多少个调用、系统里边暴露了多少个binder,在哪里触发的,都欠好搞。
因而直接来粗犷的办法,把一切binder调用抓仓库下来。
屡次复现、屡次抓取,阅览仓库、总结分类,能够抓到蛛丝马迹。由于最长的仓库高达33万行(包括合理的正常的binder和形成功用问题的binder),且抓了好几份,这儿只能将问题的要害点做个展现输出。
ls
20230209.fk.trace binder.20230209.2.fk.trace.log binder.20230209.4.fk.trace.log binder.20230209.fk.trace.log binder.20230210.1.fk.trace.log
20230209.ok.trace binder.20230209.3.fk.trace.log binder.20230209.5.fk.trace.log binder.20230209.ok.trace.log
fk表明fuck,即不正常状况下的binder仓库;ok表明正常。
其间功用毛病对应的仓库如下(几类有功用问题的binder调用;仅截取要害方位):
榜首个仓库放全一些,能够看出,在正常的traversal进程中,View系统正常调用getResources(),binder发生在getResources()内部:它调用了IWindowManager.getInitialDisplayDensity(),经过binder“飞”到system_server:
Count: 15
Trace: java.lang.Throwable
at android.os.BinderProxy.transact(BinderProxy.java:547)
at android.view.IWindowManager$Stub$Proxy.getInitialDisplayDensity(IWindowManager.java:3025)
at java.lang.reflect.Method.invoke(Native Method)
at refactor.common.base.FActivity.e5(FActivity.java:7)
at refactor.common.base.FActivity.getResources(FActivity.java:7)
at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:1)
at android.view.View.measure(View.java:25597)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7114)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1632)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:922)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:801)
at android.view.View.measure(View.java:25597)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7114)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:331)
at android.view.View.measure(View.java:25597)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7114)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1632)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:922)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:801)
at android.view.View.measure(View.java:25597)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7114)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:331)
at com.android.internal.policy.DecorView.onMeasure(DecorView.java:763)
at android.view.View.measure(View.java:25597)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3665)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2302)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2564)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2026)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8469)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:731)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:8024)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:605)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
其实这儿现已能看出问题,而且看清问题的严重性了。能够说,tarversal阶段是一个App最严重、最重要的阶段之一,在这个要害时刻窗口内,还调用了binder通信这一不可靠的办法(IPC是不可预期的),对功用影响很大。
该运用的View完成喜爱在traversal阶段调用上述Binder,包括但不限于如下几个:
Count: 5
Trace: java.lang.Throwable
at android.os.BinderProxy.transact(BinderProxy.java:547)
at android.view.IWindowManager$Stub$Proxy.getInitialDisplayDensity(IWindowManager.java:3025)
at java.lang.reflect.Method.invoke(Native Method)
at refactor.common.base.FActivity.e5(FActivity.java:7)
at refactor.common.base.FActivity.getResources(FActivity.java:7)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:221)
at android.view.View.measure(View.java:25597)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7114)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1632)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:922)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:801)
at android.view.View.measure(View.java:25597)
at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1463)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:803)
at android.view.View.measure(View.java:25597)
...
Count: 20
Trace: java.lang.Throwable
at android.os.BinderProxy.transact(BinderProxy.java:547)
at android.view.IWindowManager$Stub$Proxy.getInitialDisplayDensity(IWindowManager.java:3025)
at java.lang.reflect.Method.invoke(Native Method)
at refactor.common.base.FActivity.e5(FActivity.java:7)
at refactor.common.base.FActivity.getResources(FActivity.java:7)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:762)
at android.view.View.measure(View.java:25597)
at android.widget.RelativeLayout.measureChild(RelativeLayout.java:849)
at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:652)
at android.view.View.measure(View.java:25597)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7114)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:331)
at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:21)
at android.view.View.measure(View.java:25597)
...
到此,现已定位到了慢路径了:
- App喜爱在View的要害回调里边调用IPC,产生巨大的功用问题
- 在measure和traversal阶段不厌其烦地调用一个通常状况很少调用的接口getInitialDisplayDensity()
- View的加载阶段、traversal阶段,在measure、layout阶段因Binder IPC过度频频触发了功用问题
定论
App在多个不同的View(及其子类ViewGroup和ViewGroup的子类们),在不合适的时机频频调用了Binder,以很低的CPU占用,抢先性地完成了很卡的效果。
虽然卡顿的奉献来自不同的View调用的同名Binder,这个binder却是同一个接口(不易变的getInitialDisplayDensity(),这意味着回来值能够被缓存下来并保证有用),而且触发的直接原因是同一个——App在Context.getResources()办法内部调用了这个binder,getResources()在App运转时会被频频调用(尤其是View创建、制作阶段)。
Context.getResources()默认完成是直接回来mResources,但是会有心爱的人会override它(或经过优美的kotlin扩展函数),往里边塞入耗时的慢办法。
清晰的定位到了慢办法、卡顿根因后,还有一个严酷的问题:比照机不卡。
回答这个问题感觉像是对自己写出来的卡顿型代码有点相得益彰的感觉。不过经过剖析,排除掉竞品的优化、App在竞品的Android版别(Android版别和咱们不一样)上事务逻辑不同、竞品的系统原生逻辑就不一样(Android版别原生逻辑差异)这三个变量要素后,结合代码阅览,发现咱们的View Tree在Measure和Layout阶段,咱们自己添加的功用会比原生要调用更屡次的getResources()办法。
这在大多数状况下十分正常(逻辑上也正常,由于这个办法只有一行直接回来Resources方针实例的代码),碰到一个在超高频办法里边加慢调用、不可靠IPC的App后只能傻眼认栽。
计划
从App视点看,它错的很离谱。优化计划也很简单,去掉一个剩余的、过度的Binder调用,一般是将调用集中在要害方位(临界区)以外、缓存回来值(保证回来值没有失效的前提下重用cache)、不在高频办法里边加东西、尽量不override sdk办法等等。
在系统侧,本着拉平甚至超越竞品的愿景,相同有减少binder调用的优化方针。不论是对运用自身问题的解决,仍是对竞品的竞争性跟进,无所谓,都出手。首要有如下一些计划:
- Framework能够完成缓存,在Binder IPC宣布前查看有用性,仅在失效后真实宣布IPC
- 把咱们加进去的额定的getResources()去掉、重构
- 在严酷的竞争、高标准的要求下,会将考虑一些非标准的操作(魔改)
有用性,是指IPC对端回来的内容没有发生改动(本质上是软件维护的状况并未发生改动)。比方,从未旋转过屏幕,那么咱们上一次获取的屏幕宽高就仍然有用,不需求再次获取,而应复用缓存
终究计划2采用并取得良好效果:帧率提高几倍、跟手了,还有一点卡卡的(App自己的+原生的getResources()调用),到达和竞品共同的水准了。
参阅
- Context.getResources()源码参阅