Android性能问题分析与优化

作者: jimjayce | 来源:发表于2018-09-04 10:59 被阅读276次

    近来对之前做优化学习记录的一些知识点进行了以下简单的总结,主要集中在以下几个方面:

    1.Systrace
    2.严格模式
    3.非保护性广播
    4.Event Log 中的性能问题
    5.帧率优化
    6.冷启动流程
    7.其他常见的优化技巧
    8.ANR

    1、Systrace

    • 截至目前为止,最实用、分析最精准、最专业的工具,但上手门槛高
    • Systrace分析重经验,平时养成经常看systrace分析运行时间、线程,CPU工作频率,CPU 抢占等习惯。
    • TraceView虽然也能分析函数执行时间,但相比systrace有诸多劣势,且对于密集函数呼叫可能会高估执行时间,对跨进程调用可能会低估执行时间,无法分析出并发的影响。故不能作为性能分析的主要工具,且常会误导解决方向。适合在systrace分析后,想进一步找线索时使用
    建议配置
    • Trace duration:3 ~ 10 秒
    • Trace Buffer Size (kb):16384
    • Commonly Used Tags:用默认

    一般情况除非需要看WebView内部运作细节,不然WebView也可以关掉

    • Advanced Options

    必选:CPU Frequency、CPU Idle
    其他按需

    2、严格模式

    严格模式能查出跨进程通讯而产生的磁盘读写,优于代码评审

    解决掉严格模式问题有什么好处

    • 减缓手机越用越慢或是突然卡顿的问题
    • 提升应用启动时间及减少ANR发生概率。

    数据库存取时间长有一大部分来自以下三种原因:

    • 数据库资料持续增加,造成查询/新增/删除/修改时间拉长
    • 数据库存取遭遇其他应用同时存取磁盘,而拉长完成时间
    • 等待被其他应用以同步锁锁住的数据库

    跨进程调用产生的严格模式问题的日志样式

    • duration: 严格模式造成的时间延迟
    • 可由堆栈第一个项判断严格模式问题种类
    • 在堆栈中出现“# via Binder call with stack”字样代表该严格模式问题来自为跨进程调用
    StrictModepolicy violation; ~duration=30ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=22085639 violation=2
    at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1296)
    at android.database.sqlite.SQLiteConnection.applyBlockGuardPolicy(SQLiteConnection.java:1044)
    …
    at android.content.ContentResolver.query(ContentResolver.java:562)
    at com.android.internal.telephony.ISub$Stub.onTransact(ISub.java:124)
    at android.os.Binder.execTransact(Binder.java:582)
    # via Binder call with stack:
    android.os.StrictMode$LogStackTrace
    at android.os.StrictMode.readAndHandleBinderCallViolations(StrictMode.java:1963)
    at android.os.Parcel.readExceptionCode(Parcel.java:1665)
    …
    at android.app.ActivityThread.main(ActivityThread.java:6470)
    …
    

    将磁盘存取移到子线程执行。但须注意以下子线程的设置:

    • 建议以HanderThread或AsyncTask,让子线程中的磁盘读取以队列方式执行
    • AsyncTask优先级设置默认是Process.THREAD_PRIORITY_BACKGROUND。假如以默认的优先级执行使用AsyncTask读取重要性高的资料时,其将被分配到较少比例的处理器资源,而影响处理速度。建议处理重要资料时,先调整AsyncTask的优先级为Process.THREAD_PRIORITY_DEFAULT。
    • 数据库存取可考虑利用AsyncQueryHandler简化子线程存取数据库实作。AsyncQueryHandler的默认优先级设置已经是Process.THREAD_PRIORITY_DEFAULT,不需更动
    • 非必要的话,避免以直接以new Thread()来执行磁盘存取。这方法容易在高并发运行时,产生大量线程拖慢系统速度

    常见该解决的问题:在主线程调用SharedPreferences的commit()

    Process: com.android.browser:service
    Duration-Millis: 59
    android.os.StrictMode$StrictModeDiskReadViolation: policy=647 violation=2
    at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1137)
    at libcore.io.BlockGuardOs.access(BlockGuardOs.java:67)
    at java.io.File.doAccess(File.java:283)
    at java.io.File.exists(File.java:363)
    at android.app.SharedPreferencesImpl.writeToFile(SharedPreferencesImpl.java:567)
    at android.app.SharedPreferencesImpl.access$800(SharedPreferencesImpl.java:51)
    at android.app.SharedPreferencesImpl$2.run(SharedPreferencesImpl.java:512)
    at android.app.SharedPreferencesImpl.enqueueDiskWrite(SharedPreferencesImpl.java:533)
    at android.app.SharedPreferencesImpl.access$100(SharedPreferencesImpl.java:51)
    at android.app.SharedPreferencesImpl$EditorImpl.commit(SharedPreferencesImpl.java:455) //commit
    at com.tencent.mtt.widget.androidwidget.QBAppWidgetProvider.b(RQDSRC:275)
    at com.tencent.mtt.widget.androidwidget.QBAppWidgetProvider.a(RQDSRC:158)
    at com.tencent.mtt.widget.androidwidget.QBAppWidgetProvider$1.handleMessage(RQDSRC:137)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:147)
    at android.app.ActivityThread.main(ActivityThread.java:5451)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:970)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:765)
    

    使用SharedPreferences.Editor.apply() 取代SharedPreferences.Editor.commit() 以避免在主线程上写入磁盘

    • commit()直接产生磁盘存取于执行的线程中,会产生严格模式问题
    • apply()则产生子线程来执行磁盘存取

    常见该解决的问题:主线程调用SQLite相关操作

    Process: com.android.phone
    Duration-Millis: 44
    android.os.StrictMode$StrictModeDiskReadViolation: policy=647 violation=2
    at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1137)
    at android.database.sqlite.SQLiteConnection.applyBlockGuardPolicy(SQLiteConnection.java:1046)
    at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:847)
    at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:836)
    at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
    at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:144)
    at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:133)
    at android.content.ContentResolver.query(ContentResolver.java:506)
    at android.content.ContentResolver.query(ContentResolver.java:426) //query
    at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.onApnChanged(DataStatusNotificationService.at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.access$300(DataStatusNotificationService.java:at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService$DataSettingsObserver.onChange(DataStatusNotification
    at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.enableContentObservers(DataStatusNotificationService
    at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.access$000(DataStatusNotificationService.java:at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService$1.onQcRilHookReady(DataStatusNotificationService.at com.qualcomm.qcrilhook.QcRilHook$6.onServiceConnected(QcRilHook.java:1513)
    at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1241)
    at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1258)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:147)
    at android.app.ActivityThread.main(ActivityThread.java:5451)
    

    3、非保护性广播

    Android 针对有一些广播只能由系统发送的,并且提供了<protected-broadcast> 标记让系统级应用在AndroidManifest.xml明确宣告。在系统运作起来之后,如果某个不具有系统权限的应用试图发送“保护性广播”,AMS会抛出异常,提示"Permission Denial: not allowed to send broadcast"。

    • 系统级应用指的是Persistent 应用或user id 为SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID、NFC_UID的应用。
    • 反之,系统级应用发出的broadcast 必须宣告成”保护性广播”,否则会触发system server 记录WTF 时间而产生写入dropbox的磁盘存取,间接导致应用的主线程卡顿
    • 例外:凡是由谷歌、高通源代码定义发送的广播,即使有非保护性广播问题,也不处理
    StrictModepolicy violation; ~duration=236 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=22085639 violation=1
    at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1256)
    …
    at com.android.server.am.ActivityManagerService.addErrorToDropBox(ActivityManagerService.java:14477)
    …
    at android.util.Log.wtf(Log.java:300)at android.util.Log.wtf(Log.java:290)
    at com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:18473)
    …
    # via Binder call with stack:
    …
    at com.android.internal.telephony.imsphone.ImsPhone.sendImsNetworkStateBroadcast(ImsPhone.java:1568)
    …
    

    解决方法:在应用的AndroidManifest.xml明确宣告成保护性广播

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:androidprv=http://schemas.android.com/apk/prv/res/android
    package="com.android.server.telecom"
    coreApp="true"
    android:sharedUserId="android.uid.system”>
    …
    <protected-broadcast android:name="android.intent.action.SHOW_MISSED_CALLS_NOTIFICATION" />
    …
    </manifest>
    

    4、Event Log 中的性能问题

    Log 格式可查询/system/etc/event-log-tags

    20003 dvm_lock_sample
    (process|3),(main|1|5),(thread|3),(time|1|3),(file|3),(line|1|5),(ownerfile|3),(ownerline|1|5),(sample_percent|1|6)
    52002 content_query_sample
    (uri|3),(projection|3),(selection|3),(sortorder|3),(time|1|3),(blocking_package|3),(sample_percent|1|6)
    52003 content_update_sample
    (uri|3),(operation|3),(selection|3),(time|1|3),(blocking_package|3),(sample_percent|1|6)
    52004 binder_sample
    (descriptor|3),(method_num|1|5),(time|1|3),(blocking_package|3),(sample_percent|1|6)
    
    • 查询是否有运行时间长的binder transaction
    I/binder_sample( 5245): [com.android.internal.appwidget.IAppWidgetService,16,588,com.android.systemui,100]
    
    • 查询是否有运行时间长的content provider 操作(可能还有bug)
    I/content_query_sample(16701): [content://call_log/calls?allow_voicemails=true,_id/number/voicemail_uri,new = 1
     AND type = ?,date DESC,906,com.android.contacts,100]
    
    • 查询是否等待synchronized 太久
    I/dvm_lock_sample( 922): [system_server,1,PowerManagerService,523,PowerManagerService.java,1787,-,858,100]
    
    • 界面启动时间:am_activity_launch_time
    1551 1573 I am_activity_launch_time: [0,248545817,com.android.browser/com.android.browser.BrowserLauncher,4432,4432]
    //倒数第二个参数就是框架触发这个界面启动直到应用画完第一帧的时间
    

    界面启动时间过长,不仅使用者会感到卡顿,严重会导致Input/Key dispatch timeout ANR
    代码优化要求:在一般功能性测试中,需在800ms 内完成。以systrace分析问题,找出主要耗时点才进行优化。如:

    • 简化布局
    • 简化在主线程的长执行时间的代码
    • 将磁盘读写移到子线程
    • 若需要做跨进程通讯,尽量移到子线程
    • 数据库查询时间:content_query_sample
    • 当数据库查询时间> 500ms 会印出
    • 当数据库查询时间< 500ms 则随机印出
    image.png
    • 跨进程通信执行时间:binder_sample
    • 当执行时间> 500ms 会d印出
    • 当执行时间< 500ms 则随机印出
    image.png

    如何找到接口编号对应的函数?(以ITelephony的接口编号40为例)

    • ROM 编译完后,所有AIDL 编译出的Java 代码,放在out/target/common/obj下。直接在该文件夹以及子文件夹查找ITelephony.java 文件:

    1、打开ITelephony.java文件
    2、找到FIRST_CALL_TRANSATION位置
    3、所有的接口编号都由IBinder的第一个接口编号1号起算。在本例中,要找android.os.IBinder.FIRST_CALL_TRANSATION+39

    • 某些模块可能自行撰写Binder 调用,不使用AIDL。此种情况需要从模块代码寻找。例如frameworks/base/core/java/android/app/IActivityManager.java
    image.png image.png image.png
    • 同步锁等待时间:dvm_lock_sample,主线程不要被同步锁卡住超过50ms。
    • 等待时间> 500ms 会印出
    • 等待时间< 500ms 则随机印出
    image.png

    5、帧率优化

    • 屏幕刷新率(Refresh):屏幕内在一秒刷新屏幕的速度
    • 手机一般要求60Hz
    • 帧率(Frame Rate):软件系统一秒绘制的帧数
    • 不同步问题
    • 帧率 > 刷新率:导致画面撕裂(screen tearing) 问题,有两个或以上帧显示在同一个frame buffer上
    • 刷新率 > 帧率:如果画面衔接不够连续,就会感觉卡顿。如果帧率= ½ 刷新率,稳定输出,感觉就没那么明显
    image.png
    • V-sync (Vertical Synchronization)
    • 保证屏幕刷新过程,内容不会变更
    • 屏幕刷新时,只从frame buffer 取
    • Vsync信号来时,才触发上层软件刷新back buffer
    • 不管MDP/GPU 多快,都应适应屏幕刷新的速度
    • Back buffer 可以超过一个
    1000ms/60 frames = 16.666 ms / frame
    //在Vsync机制下,想保持流畅的体验,每帧必须在16.6ms 内完成
    
    • SurfaceFlinger


      image.png
    • Choreographer

    • 工作流程:请求Vsync→ 收到Vsync→ 请求Vsync→ 收到Vsync
    • 如果没有再次请求Vsync,则无法收到
    image.png
    • Rasterization(光栅化)
    image.png
    • Display List
    • 记录(Record) 一个View 的绘制需求
    • 在HWUI内部处理,还需要进行其他的预处理,例如判别显示区域、产生HWlayer 等,才能转换变成OpenGL 指令
    • 只要View的属性/内容不变,就不需要重新产生Display List

    改变的时机、频率至关重要,多余的view invalidate、重新measure/layout 等,都会触发display list改变(CPU运算)。有些对应的OpenGL会相当耗时,例如重新upload texture、重新产生HW layer 等。

    image.png
    • GPU 呈现模式分析

    虽然工具叫GPU 呈现模式分析,但其实显示的内容不只是GPU 工作时间

    image.png

    GPU 呈现模式分析用在什么场合

    • 对解决问题而言,基本没用。信息太概略,无法知道问题在哪里,Systrace才能分析根因
    • 以发现问题的角度而言,容易造成测试与研发误解,花时间优化不重要的事
    • 有些帧处理的时间稍长,未必导致卡顿。例如第一帧一般准备工作比较多。中间偶尔掉一帧,其实用户看不出来
    • 应用只要有界面刷新,工具就会显示长条。不容易看出滑动、动画播放精准的开始与结束点

    在Triple Buffer 架构下,应用即使掉帧,也可能还有layer buffer 能画


    image.png

    上面的流程仔细体会。

    • 硬件加速与渲染线程

    UI 线程

    • 每个View 透过draw(),将绘制的需求,透过DisplayListCanvas,记录在RenderNode里的DisplayList

    渲染线程:兼顾性能优化、抽象层次

    • Defer: 对RenderNode里DisplayList的渲染需求,进行前处理,主要是进行Batch与Merge,产生一群BatchBase,对应一群渲染操作,减少后续GL draw call
    • FrameBuilder:管理每帧的绘制任务,调用各个LayerBuiler进行
    • LayerBuilder:管理每帧中,某一层的绘制任务。可能对应目前的Surface 或一个FBO。将BatchBase透过BakedOpDispatcher调用到BakedOpRender、RenderState,再透过Glop真正执行OpenGL 指令
    image.png image.png

    以上的流程可以通过在systrace上加深体会。

    • 避免过度绘制
    • 避免大面积的过度绘制
    • 去掉WindowBackground
    • 若自行实现复杂的View,可在onDraw内,还可透过下列几种方法,减少过度绘制
    • 使用Canvas.clipRect(float left, float top, float right, float buttom) :设置显示范围


      image.png
    • 使用Canvas.quickReject(float left, float top, float right, float bottom, EdgeTypetype) :若回传true,代表参数所指定的矩形区域,完全在目前的canvas clip 外。此时就可忽略那些对应的渲染需求。
    • 使用Hardware Layer

    适用时机:布局复杂ViewGroup的大面积移动、移动过程中不会改变内容

    • 观念:将每帧做的复杂绘制运算,提前在动画启动前做一次
    • 避免误用:产生hardware layer是耗时操作,不应在动画过程中改变或产生hardware layer。例如不应使用在ListViewitem
    // 启动动画时设为hardware layer
    viewGroup.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    // 动画结束时,记得关掉hardware layer,释放内存
    viewGroup.setLayerType(View.LAYER_TYPE_NONE, null);
    // View animation 过程中开启hardware layer
    ViewPropertyAnimator.alpha(0.5f).withLayer();
    
    • 使用hardware layer必须保证动画过程没有内容更新,否则会造成掉帧
    • 开发者选项 > 显示硬件层更新

    只要hardware layer 产生或变更内容,就会有绿色的闪烁

    • Alpha 效果优化
    • View alpha 属性的影响
      必须先知道下层的元素是什么,再结合这个View进行混色处理
    • 尽可能避免大面积的alpha动画
      Alpha 效果常伴随过度绘制的问题
    • 减少alpha效果的影响
      setAlpha()会造成硬件加速呼叫saveLayer()进行复杂操作
      呼叫setAlpha()的View优化方向
    • 不做其他绘制,例如设定background、复写onDraw()
    • 复写hasOverlappingRendering()并回传false
    • 产生hardware layer
    • ListView优化
    • 较大图片的加载,不适合在每个item的getView()中读取,会导致掉帧。一般解决做法是:
      在子线程加载图片。
      若滑出这页,则取消对应的图片加载,停止滑动时,才将图片设进ImageView。

    6、冷启动流程

    • 前一个应用的pause 算在启动时间内。如果是跨进程启动,pause 超过500ms 就会触发pause timeout,强迫画面切换。
    • am_activity_launch_time只计算到IdleHandler触发时的时间,并非完整的界面加载完时间。
    • 从startActivity开始,历经前一个activity的pause,到下一个activity画出第一个画面的时间。故又称display time。
    • 若第一个activity用于跳转,也就是onCreate执行完就startActivity并finish自己,则会计算到下一个activity 画出第一个画面的时间。
    • am_activity_fully_drawn_time
    • 应用可自行在数据加载完,调用Activity.reportFullyDrawn(),告诉AMS 真正执行完的时间,供自己调试用
    • 这个跨进程调用可能会增加不必要的卡顿,建议在子线程调用。
    • 提升启动速度的方法:减少插入主线程执行的handler、减少跨进程通信、减少下面几个重点函数的执行时间。
    • 在Application.onCreate() 初始化太多模块,拖慢进程启动时间。
    • 在onCreate()、onResume()中post Message、Runnable到主线程,导致画面绘制的Handler 被推迟执行
    • 不同模块重复调用同样的耗时接口,如AMS、WMS 接口。
    • 在主线程调用数据库的读、写或执行bindService、provider 等需要等待其他线程的操作。
    • 在主线程调用registerReceiver等容易被AMS耽误执行时间的接口。
    • 在主线程初始化第三方SDK,而第三方SDK又有耗时操作。
    image.png

    7、其他常见的优化技巧

    • 用System.arraycopy() 取代循环语句赋值
    final int size = 1000000;
    int[] array1 = new int[size];
    int[] array2 = new int[size];
    // 注意:size 大时,才有明显效果
    System.arraycopy(array1, 0, array2, 0, size);
    //for (inti= 0 ; i< size ; ++i)
    //  array2[i] = array1[i];
    
    • ArrayList、Vector、HashMap、HashSet等,可以在初始化时,根据程序可能面对的数据量,指定初始容量,避免过程中的内存重新分配与内容复制。
    ArrayList<Foo> a = new ArrayList<Foo>(20);
    
    • 安卓不适合使用SoftReference。对象几乎不会被GC,反而导致内存问题

    因为 SoftReference 无法提供足够的信息可以让 runtime 很轻松地决定 clear 它还是 keep 它。 Android推荐使用android.util.LruCache做Cache管理。

    • 减少GC
    • 避免在关键路径中,频繁分配内存,以减少GC 可能带来的卡顿。例如:View.onDraw()中新建新对象。

    过度采用这个策略,将对象宣告为static,导致两个问题:

    • 内存无法释放:有些Java对象乍看虽小,但native 分配的内存却不小,例如将正则表达式Pattern宣告为static。
    • static对象在类加载时就会初始化,即使最后代码没走到,也会多花执行时间。
    • 针对需要频繁分配、释放的小对象,采取Object Pool 方式优化,减少虚拟机分配内存时间、减少GC
    • AsyncTask陷阱
    • AsyncTask与Activity、Fragment 的生命周期不同步,不随着onDestroy而消灭
    • 若执行任务长,可能会导致连续进出Activity、Fragment 的情况,有太多线程运行,导致卡顿。更坏情况,例如用AsyncTask执行非常耗时的ContentProviderquery,可能连续进出几次,就把provider 进程的Binder 线程全占满
    • 可能onPostExecute() 执行时,前一个Activity/Fragment 已经销毁,现在运行的,可能需要的数据不同。处理不好容易导致功能异常
    • 自己撰写的AsyncTask,若不是static 类,隐性持有外部的Activity、Fragment,会导致他们无法被GC
    • AsyncTask产生的子线程,默认的优先级是THREAD_PRIORITY_BACKGROUND。若此任务需要尽快执行,则这个优先级会导致执行完的时间不确定
    • AsyncTask以ThreadPoolExecutor实现,每个实例会产生线程池。应用中过多的AsyncTask实例,会导致线程过多
    • 避免应用在后台执行不必要的任务
    • 在后台,仍然触发画面刷新
    • 同一个应用,多个Receiver 注册同一个intent,如SCREEN_ON
    • ContentObserver监听到Uri 变化后,就在后台马上query
    • 举例:遇到联系人同步的情况,就是系统性灾难
    • 应该等到应用回到前台,才query
    • 使用getInstalledApplications()、getInstalledPackages() 等类似的函数,读取各应用的字符串,触发大量I/O、CPU运算
    • 线程优先级不能乱设定
    public class Process {
        public static final intTHREAD_PRIORITY_DEFAULT = 0;
        public static final intTHREAD_PRIORITY_LOWEST = 19;
        public static final intTHREAD_PRIORITY_BACKGROUND = 10;
        public static final intTHREAD_PRIORITY_FOREGROUND = -2;
        public static final intTHREAD_PRIORITY_DISPLAY = -4;
        public static final intTHREAD_PRIORITY_URGENT_DISPLAY = -8;
        public static final intTHREAD_PRIORITY_AUDIO = -16;
        public static final intTHREAD_PRIORITY_URGENT_AUDIO = -19;
        public static final intTHREAD_PRIORITY_MORE_FAVORABLE = -1;
        public static final intTHREAD_PRIORITY_LESS_FAVORABLE = +1;
    }
    
    • 设成THREAD_PRIORITY_BACKGROUND、LOWEST 等,必须是不紧急的任务,不要求在UI立即显示。
    • 应用不允许设成比THREAD_PRIORITY_DEFAULT 更高的优先级,会影响自身与系统性能、抢占CPU、导致许多莫名的时序问题出现。
    • View 层级简化
    • ViewStub
    • 分页加载(处理Tab、ViewPager的情况)

    原始做法:一次性产生所有Fragment 与其下所有的View。启动性能差、且刚启动时由于大量UI 在初始化化,主线程过多任务,应用容易卡顿

    • 改进方法1:看不到的Fragment,推迟到开始滑动到那个Fragment 才产生
    • 改进方法2:Fragment 先产生,但View 推迟到开始滑动过去那个Fragment 才inflate、measure、layout
    • 改进方法3:Fragment 先产生,而View 在子线程inflate。若有多个Fragment,则依照与目前可视的Fragment,由近向远依次产生、或滑动到相邻页才产生。但inflate 后,不能马上加入View阶层,否则马上会触发在UI线程的measure与layout,导致卡顿。等到要开始滑动时,才加入View 阶层。
    • Constraint Layout
    • Bitmap

    常是内存问题的来源

    • 全屏ARGB 图片:1920 x 1080 x 4 = 8294400 = 8 MB
    • 1600万像素照片:4608 x 3408 x 4 = 62816256 = 60MB

    常是性能问题的来源

    • 在UI线程加载大图片
    • 加载比实际显示区域大的图片
    • 显示区域与图片大小不符

    使用LRU (Least Recently Used) Cache

    • 数据库优化
    • 常运行、或短时间内高频度运行的类似语句,可以使用prepared statement,减少每次都要重新编译SQL 语句再执行的时间
    • 减少transaction 次数
    • bulkInsert() 的实现,依赖自行控制transaction 提升性能
    • 使用applyBatch(),能一次将不同种类的操作,打包处理

    安卓默认的Provider.applyBatch() 实现,仅是一个个执行,并未使用单次transaction 提升性能。

    • applyBatch() 或bulkInsert() 单次调用,需要有笔数上限,笔数太多,需要拆分多个批次执行。
    • 单次binder transaction 允许传递的内存,避免应用发生transaction fail
    • 锁住数据库的时间太长,会导致其他应用无法访问
    • 建议:
    • 传递数据量:不超过128 KB
    • 执行时间:不超过100ms
    • 经验:一般应用,一次小于50笔数据
    • SQLite的index
    image.png
    • CPU调度

    cpuctl (Android N 及之前默认开启)
    控制不同线程之间抢占 CPU 的分配

    • Android以 cpuctl 机制,设计了 foreground scheduling 与 background scheduling,当两种线程同时竞争 CPU 资源时,采95:5 的方式,优先分配给 foreground scheduling
    • OOM_ADJ <= 1 的安卓应用:Foreground cpuset
    • 其他安卓应用:Background cpuset

    8、ANR

    • 三种ANR
    • Input (key) dispatching timeout:System_server 送出输入事件到某窗口后,应用无法在 5秒内完成,让框架通知 System_server 此次输入事件已处理完成。若在应用启动过程,在画出第一帧(也就是 am_activity_launch_time 出现之前)窗口是无法响应输入的。
    • Service timeout:System_server 送出 intent 到应用启动 service,过程中可能包含进程启动、应用执行 onCreate() + onStartCommand(),总时间无法在 X 秒内完成并通知System_server
    • 应用在前台时: X = 20 秒
    • 应用在后台时: X = 200 秒
    • Broadcast timeout: System_server 送出 intent 到应用,过程中可能包含进程启动、应用
      执行 onReceive(),总时间无法在 Y 秒内完成并通知 System_server
    • Foreground broadcast:Y = 10 秒
    • 其他 broadcast:Y = 60 秒

    假设在某段时间执行如下:


    image.png

    从中可以看到导致ANR发生的原因可能并不是因为某个特定的服务或广播时间超过限制了,而是由于主线程赛车导致的整体的时间的延长。
    所以有一个基本观念就是:

    • 主线程运行任务多、执行时间长,导致 ANR 的可能性就越大
    • ANR 解决的是主线程塞车的问题

    由上可知,ANR是比较难解且无法从 log 得知为何执行突然变慢。到底是数据量大、还是被其他进程卡住了、还是CPU 被其他应用抢占了、还是系统关核限频了,目前的解法就是:

    • 抓 systrace
    • 错误的线程优先级设定
      就是优先级倒转问题,使得主线程等待子线程执行
    • 若子线程设成 THREAD_PRIORITY_BACKGROUND,则更有可能导致 CPU 资源被抢走。故无法避免情况,至少要将子线程设成 THREAD_PRIORITY_DEFAULT
    • AsyncTask 默认的优先级是 THREAD_PRIORITY_BACKGROUND,需要特别留意

    下面看一个优先级倒转的例子

    image.png

    上图中T1最先开始运行并获得了R锁,之后T2、T3、T4也都运行,之后T3、T4等待R锁,此时由于T1优先级比较低,按照上面我们讲的95:5的CPU分配比例,不需要R锁的T2,就开始抢占T1运行的时间,导致T3、T4的等待时间将完全取决于低优先级的T2,如果此时还有更多的低优先的线程在抢占资源那么T3、T4的执行将会很糟糕。

    看段Log从中可以看到一些线程的设置信息

    "OnLineMonitor" prio=5 tid=17 Native
    | group="main" sCount=1 dsCount=0 flags=1 obj=0x12e41dc0 self=0xe61fd200
    | sysTid=14587 nice=0 cgrp=default sched=0/0 handle=0xcf3fb970
    | state=S schedstat=( 303078149 1485237159 1965 ) utm=23 stm=7 core=2 HZ=100
    | stack=0xcf2f9000-0xcf2fb000 stackSize=1038KB
    | held mutexes=
    kernel: (couldn't read /proc/self/task/14587/stack)
    native: #00 pc 00018de0 /system/lib/libc.so (syscall+28)
    native: #01 pc 000b76a1 /system/lib/libart.so
    (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+88)
    native: #02 pc 003e6c89 /system/lib/libart.so (art::GoToRunnable(art::Thread*)+308)
    native: #03 pc 003e6b21 /system/lib/libart.so (art::JniMethodEnd(unsigned int, art::Thread*)+8)
    native: #04 pc 0075d969 /system/framework/arm/boot-framework.oat
    (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+144)
    at android.os.BinderProxy.transactNative(Native method)
    at android.os.BinderProxy.transact(Binder.java:793)
    at android.app.IActivityManager$Stub$Proxy.getProcessMemoryInfo(IActivityManager.java:6324)
    at java.lang.reflect.Method.invoke(Native method)
    at c8.ky._1invoke(Interception.java:-1)
    
    • nice=0 代表 THREAD_PRIORITY_DEFAULT, nice = 10 代表 THREAD_PRIORITY_BACKGROUND
    • cgrp=defult 代表 foreground CPU scheduling,cgrp=bg_non_interactive 代表 background CPU scheduling
    • 在主线程调用系统服务,例如 AMS、WMS、PMS 等,可能在系统高负载、高并发运行下遇到ANR,原因在于这些接口一般是阻塞式的,而安卓框架内有太多同步锁、彼此影响。故主线程应尽量减少这些接口的调用

    常见可能会变得很慢的调用:registerReceiver、sendBroadcast

    相关文章

      网友评论

      本文标题:Android性能问题分析与优化

      本文链接:https://www.haomeiwen.com/subject/uxmfwftx.html