1、概述
手机性能优化越来越重要,本文介绍了性能评估、分析和优化的方法。在接下来的章节中,将会分别介绍关于系统、开机启动时间、应用启动速度性能优化,同时也会介绍如何使用一些有用的工具去评测性能瓶颈。
可能对性能产生影响的方式有多种,例如:测试方法、系统配置和驱动的修改。因此,首先要分析环境对性能瓶颈的影响,通过消除不同测试方案中的配置能够找到产生性能瓶颈的原因。
同样要注意到低内存的配置也会影响性能。在低内存的环境中,有可能启动linux kernel的内存压缩机制。这将会使性能降低取决于在运行态压缩和被压缩的内存。另外关于低内存使用的问题,总内存大小不会使所有的进程都处于存在的状态,一些进程会不停的杀死和重启在一定程度上会影响性能。低内存的配置将会导致cache的大小小于正常的水平,因此性能可能会受到影响。
除了环境因素外,一些工具被用来分析性能瓶颈问题。性能瓶颈可能会发生当意想不到的应用运行在后台时跟前台UI进程竞争系统资源(CPU或内存)。此时可以使用工具”top”查看CUP占有率情况。
2、系统性能
2.1 本节简介
首先,我们介绍一些正确的评测方法和有用的工具去分析高效的分析性能下降的根本原因。并举例说明,你可以参考一下并快速定位同一类问题产生的根本原因。
2.2 如何缩小可能的根本原因?
2.2.1 性能评测和环境搭建
在搞清楚瓶颈或优化性能之前,我们必须建立一个具体的目标和建立一个准确的评测环境。
1、量化性能:定义性能
首先需要定义性能的好或差,虽然用户体验可以感觉出来,但是我们需要量化性能的状态。例如:应用启动时间太慢是啥意思?是指用户等待应用布局界面完全显示出来,或者用户等待所有的功能都准备好么?如:浏览器需要完全加载web网页信息。
2、环境定位:准备的评估
硬件定位:系统的行为会受到各种因素的影响,尤其是后台进程或者监控进程,例如:Wi-Fi、USB、日志系统(adb)。同样外设可能产生不同的结果,如:SD卡。我们必须评估性能在同样的环境中,例如:同样的SD卡,没有USB连接。
软件定位:首先确保你的代码版本是正确的,如:打上补丁的最新版本;其次,纯净的代码库,意味着你的代码不包括其他不相关的修改。
2.2.2 有用的分析工具
有一些从系统级别到用户级别的分析工具可以帮助分析性能。通过恰当的使用这些工具,提供用户指导在其他章节。本节介绍何时使用这些工具。
1、搞清楚系统的脚本
[ftrace-scheduleswitch tracer]通过ftrace显示进程/中断的CPU占用情况,一般情况下,这个工具能够帮助:
1) 确保是否有任何不期望的后台进程或监控程序;
2) 确保每个进程的执行序列;
3) 在特定时间段每个进程或中断的执行时间;
4) 验证是否有不需要的空闲时间
2、找到可能的瓶颈
理解了系统脚本和它正常的行为,如:没有后台进程/监控程序,没有不必要的空闲时间。然后需要进一步找可能的瓶颈。有两个有用的工具可以帮助验证可能的瓶颈。
[cputime – Athread level profiling tool]它提供了每个进程在特定时间段的执行时间;
[oprofile –Function/Library level profiling tool]它提供了一个统计的结果显示每个函数或库执行的频率。
3、分析可能的瓶颈
一旦找到可能的瓶颈,例如:某个进程、应用、库、函数,可以使用更精确的工具去分析它,通过在源代码里嵌入相应的调试方法。
[Traceview] 分析工具可以帮忙分析java应用程序;
[Insertdebug log] 稳健和精准的方法。SW开发者可以嵌入他们的计算器API去评估每一个热点或打印出事件日志去验证SW阶段的持续时间。
2.3 内存压缩(ZRAM)
2.3.1 什么是ZRAM?
对于低成本的项目,的内存空间有限,在android系统里面会运行大量的进程。内核、驱动、服务、后台监控程序占据内存,留下非常有限的内存给应用。为了更大化的利用内存,可以启用一种内存压缩机制叫做ZRAM。当内存不充足时,内核将会对最近不使用的内存进行压缩,在运行时获取更多的剩余内存可以使用。当被压缩的内存再次使用时,内核将会在使用之前解压缩它。因为并不是所有的内存都在运行时使用的,这种方式很有用。
2.3.2 缺陷
ZRAM并不是一个完美的解决方案,它也会引起一些问题。如:在压缩和解压缩内存过程中对CPU的消耗。如果内核频繁进行压缩和解压缩内存的操作,将会对CPU占用时间产生影响,进而导致性能下降。
2.3.3 ZRAM
在系统能够负担的起的情况下,使用更多的内存,大多数内存被压缩,然后被立即使用。在这种情况下,内存将会不停地压缩和解压缩,叫做ZRAM Thrashing。
2.3.4 如何检查影响
ZRAM痛打可能会影响性能,要注意以下几点:
2.3.4.1 cat/proc/vmstat
ZRAM是基于Linux交换机制,每个页在换出过程中压缩,在换进过程中解压缩。因此可以通过访问pswpin、pswpout的值来检查内核是否在进行ZRAM压缩和解压缩。由于内核内存的回收将会释放一些不使用的页面缓存,一旦空闲的页面缓存再次使用的时候,将会导致文件映射故障过程,并从存储中获取页面缓存。应该通过访问pgfmfault的值去检查加载的过程。
可以通过查看SwapTotal获取zram的总大小,通过SwapFree获取可以使用的zram大小。
2.3.4.3 cat /proc/zraminfo
不同的场景压缩率是不一样的。在大多数情况下,压缩率可以达到30%,在多媒体场景下可以达到40%。DisSize相当于SwapTotal;OrigSize意思是压缩页的原始大小;ComprSize意思是压缩页的压缩大小;MemUsed意思是实际内存使用情况。
2.3.4.4 cat/proc/mlog or dmlog
系统时刻会记录重要的内存信息,这些数据将保存在一个环形缓冲区,一旦数据超过大小,将会被覆盖。可以使用cat /proc/mlog 或dmlog命令获取内存日志信息。日志格式如下所示(PS:高通8916 5.1平台不支持)
2.3.5 如何防止Thrashing问题
ZRAMThrashing问题是由于较高的内存使用引起的,要从内存优化方面入手避免产生这种现象。
2.3.5.1 低内存被杀(Low memory killer)
LMK能够减少这种痛打问题。当内存不充足时,LMK将会杀死后台的java进程获取更多的内存。这将充分利用zram内存回收,并防止导致系统性能下降。尽管如此,LMK只会杀死那些优先级大于0的java进程。如果有很多常驻进程,LMK不会杀死它们,可能会导致zram颠簸。
2.3.5.2 减少内存使用率
换句话说,如果总的内存使用率比较高的话,应该尽量减少内存的使用。它可能是由于内存泄漏或异常的内存使用引起的,可以通过使用procrank(命令:procrank -s)或者librank来检查内存使用明细。
2.4 进程颠簸问题
2.4.1 进程颠簸的定义
进程颠簸定义为:“进程在一段时间内不停地被杀死然后又重启,这种被杀/重启占据大量的时间,会使性能变差。”
2.4.2 如何识别进程颠簸
进程颠簸跟进程杀死/重启相关的,进程的生命周期可以从event日志中分析。
2.4.3 如何处理进程颠簸
1)修改LMK的阀值
2)使进程的adj优先级变小
3)让进程使用较少的内存(java进程合并、java进程转变成native进程、改变用户场景)
2.5 ANR(Application Not Responding)
2.5.1 概览
本章将会描述ANR的基本概念,调试技巧,并给一些有用的指导去分析解决ANR的问题。
2.5.2 基本概念
在android系统中,用户在一段时间内操作应用没有及时响应的话,就会弹出一个对话框,即应用无响应对话框,包含两个选择按钮:强制关闭和等待。
2.5.3 ANR类型
应用程序响应受到AMS、WMS系统服务监控,一般在满足如下条件之一时对于特定的应用android才会显示ANR对话框:
1、按键分发超时:对于输入事件(按键、触屏)在8s内无响应
2、广播超时:前台广播接收器在10s内没有处理完;后台广播接收器在60s内没有处理完
3、服务超时:在20s内请求服务失败
2.5.4 ANR日志分析方法
ANR的分析
将基于MTK平台讨论ANR的一般解决思路,旨在通过了解本文后能够对ANR问题能够快速定位,减少排查的时间。
最好使用userdebug版本测试,因为ENG版本测试的话本身会加重CPU loading
获取日志还有一点需要注意,发生ANR后,不要选择结束进程,因为这样AMS会kill掉该进程,有些信息会打印不出来,最好是ANR发生后等两三分钟左右,再将mtklog收集起来(db.XX.ANR写入到aee_exp文件夹下需要时间)。这里的aee_exp文件夹一般都是需要的, 对DB进行dump解析,得到ANR发生时场景信息,比如主线程callstack,CPU,memory等,在分析问题根因时很关键。
日志分析一般步骤
Step1:
查看mobilelog文件夹下的events_log,搜索关键字"am_anr",这一步用于确认ANR时间点,可以搜索到类似如下信息
11-09 21:33:21.518: I/am_anr(1041): [0,1848,com.android.contacts,953728589,Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 1.)]
这个例子中的ANR类型为Input dispatching timed out, 这种anr的原因的是在viewrootimpl分发事件时,并没有找到focuswindow导致的。时间点:21:33:21,进程号:1848 注意这里的进程号比较重要,后面要去trace.txt或者db文件中去看这个process(1848)对应的backtrace
Step2:
查看mobilelog文件夹下的main_log,搜索关键字"Application is not responding",发生ANR的时间或DB(MTK工具GAT)解析的_exp_main.txt
Backtrace:
Process: com.android.contacts
Flags: 0x38d8be4d
Package: com.android.contacts v24 (1.2)
Foreground: Yes
Activity: com.android.contacts/com.mediatek.contacts.list.ContactListMultiChoiceActivity
Subject: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 1.)
Build: D-171117V33:user/release-keys
nullAndroid time :[2017-11-19 03:31:26.86] [107321.313]
ANR期间的 Cpu usage,前几个占用情况以及Total如下:
CPU usage from 74494ms to 0ms ago (2019-11-09 21:32:06.966 to 2019-11-09 21:33:21.460):
104% 323/mobile_log_d: 17% user + 86% kernel / faults: 2622 minor
56% 1041/system_server: 41% user + 14% kernel / faults: 30698 minor 36 major
34% 1408/com.android.phone: 28% user + 6% kernel / faults: 17027 minor 35 major
15% 5541/android.process.acore: 13% user + 2.3% kernel / faults: 10865 minor 49 major
10% 248/servicemanager: 4.4% user + 6.5% kernel / faults: 63 minor
10% 249/surfaceflinger: 5.1% user + 5.6% kernel / faults: 1645 minor 1 major
8.6% 215/logd: 2.7% user + 5.8% kernel / faults: 310 minor 11 major
5.6% 1848/com.android.contacts: 4.4% user + 1.1% kernel / faults: 9569 minor 677 major
3.7% 1155/com.android.systemui: 2.9% user + 0.7% kernel / faults: 12145 minor 50 major
98% TOTAL: 54% user + 42% kernel + 0.1% iowait + 0% softirq
很多时候需要注意iowait的占有率,如果占比比较高,则排查的方向要倾向与读取文件操作有关的信息,可以看trace日志中有没有读取文件或者操作SD卡的动作
还有注意观察一下在出现“Application is not responding”信息往上看,注意主线程是不是有耗时的动作,可以搜索关键字"ActivityThread"且搜索到的片段含有ANR的进程名。
如果发现ANR时CPUloading并不高,再去检查memory的情况,以排查是否存在竞争memory的情况。aee_exp文件下的db文件解析后会有一个 SYS_MEMORY_INFO文件,这个文件会列出问题发生时的MemTotal,MemFree,MemAvailable等信息。
Step3:
查看时间段内对应线程的 call stack
----- pid 1848 at 2019-11-09 21:33:21 -----
Cmd line: com.android.contacts
Build fingerprint: 'TECNO/H3721A1/TECNO-IN5:7.0/NRD90M/A-171108V56:user/release-keys'
ABI: 'arm64'
Build type: optimized
Zygote loaded classes=4375 post zygote classes=1641
Intern table: 50890 strong; 232 weak
JNI: CheckJNI is off; globals=848 (plus 2739 weak)
Libraries: /system/lib64/libandroid.so /system/lib64/libcompiler_rt.so /system/lib64/libdcfdecoderjni.so /system/lib64/libjavacrypto.so /system/lib64/libjnigraphics.so /system/lib64/libmedia_jni.so /system/lib64/libwebviewchromium_loader.so libjavacore.so libopenjdk.so (9)
Heap: 6% free, 35MB/37MB; 294645 objects
Dumping cumulative Gc timings
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x75740b50 self=0x7128096a00
| sysTid=1848 nice=0 cgrp=default sched=0/0 handle=0x712c693a98
| state=S schedstat=( 343191137004 166931064193 540669 ) utm=28155 stm=6164 core=0 HZ=100
| stack=0x7fdc4ca000-0x7fdc4cc000 stackSize=8MB
| held mutexes=
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:622)
at android.database.BulkCursorProxy.getWindow(BulkCursorNative.java:163)
at android.database.BulkCursorToCursorAdaptor.onMove(BulkCursorToCursorAdaptor.java:82)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:236)
at android.database.CursorWrapper.moveToPosition(CursorWrapper.java:197)
at com.android.common.widget.CompositeCursorAdapter.getItem(CompositeCursorAdapter.java:436)
at com.mediatek.contacts.list.MultiPhoneAndEmailsPickerAdapter.getDataId(MultiPhoneAndEmailsPickerAdapter.java:230)
at com.mediatek.contacts.list.DataKindPickerBaseAdapter.getItemId(DataKindPickerBaseAdapter.java:249)
at android.widget.HeaderViewListAdapter.getItemId(HeaderViewListAdapter.java:194)
at android.widget.AdapterView.rememberSyncState(AdapterView.java:1255)
at android.widget.AbsListView.setItemChecked(AbsListView.java:1142)
at com.mediatek.contacts.list.AbstractPickerFragment.updateCheckBoxState(AbstractPickerFragment.java:742)
at com.mediatek.contacts.list.AbstractPickerFragment.onSelectAll(AbstractPickerFragment.java:261)
at com.mediatek.contacts.list.ContactListMultiChoiceActivity$1.onClick(ContactListMultiChoiceActivity.java:383)
at android.view.View.performClick(View.java:6076)
at android.widget.CompoundButton.performClick(CompoundButton.java:122)
at android.view.View$PerformClick.run(View.java:23138)
at android.os.Handler.handleCallback(Handler.java:836)
at android.os.Handler.dispatchMessage(Handler.java:103)
at android.os.Looper.loop(Looper.java:203)
at android.app.ActivityThread.main(ActivityThread.java:6292)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1094)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:955)
可以看到在等待binder返回
Step4:
binder分析 binder_analysis
[Info]: Start parse executing & pending transactions for 1848!
incoming transaction 118494543: 0000000000000000 from 1848:1848 to 5541:5554 code 1 flags 10 pri 0 r1 node 118458515 size 68:0 data 0000000000000000
[Analysis]: This binder call is waiting for 5541:5554 to executing complete.
outgoing transaction 118494543: 0000000000000000 from 1848:1848 to 5541:5554 code 1 flags 10 pri 0 r1 node 118458515 size 68:0 data 0000000000000000
[Analysis]: This binder call is waiting for 5541:5554 to executing complete.
pending transaction 118494731: 0000000000000000 from 0:0 to 1041:3702 code 0 flags 0 pri 0 r0 size 8:0 data 0000000000000000
[Analysis]: This binder call is waiting for 1041:3702 to executing complete.
查看SYS_PROCESSES_AND_THREADS 线程的信息
对端1041(system_server):3702(1041_D)
"Binder:1041_D" prio=5 tid=61 Native
| group="main" sCount=1 dsCount=0 obj=0x14b0c160 self=0x7110f02200
| sysTid=3702 nice=0 cgrp=default sched=0/0 handle=0x71078e1450
| state=S schedstat=( 775971117390 490089600857 2178398 ) utm=57038 stm=20559 core=2 HZ=100
| stack=0x71077e7000-0x71077e9000 stackSize=1005KB
| held mutexes=
kernel: (couldn't read /proc/self/task/3702/stack)
native: #00 pc 000000000001bcec /system/lib64/libc.so (syscall+28)
native: #01 pc 00000000000e77c8 /system/lib64/libart.so (_ZN3art17ConditionVariable16WaitHoldingLocksEPNS_6ThreadE+156)
native: #02 pc 000000000054aaac /system/lib64/libart.so (_ZN3artL12GoToRunnableEPNS_6ThreadE+328)
native: #03 pc 000000000054a920 /system/lib64/libart.so (_ZN3art12JniMethodEndEjPNS_6ThreadE+28)
native: #04 pc 000000000088b6a0 /system/framework/arm64/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+220)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:622)
at android.os.ServiceManagerProxy.getService(ServiceManagerNative.java:123)
at android.os.ServiceManager.getService(ServiceManager.java:55)
at android.telephony.SubscriptionManager.getPhoneId(SubscriptionManager.java:1020)
at com.android.server.TelephonyRegistry.idMatch(TelephonyRegistry.java:1983)
at com.android.server.TelephonyRegistry.notifySignalStrengthForPhoneId(TelephonyRegistry.java:901)
- locked <0x0bb31bba> (a java.util.ArrayList)
at com.android.internal.telephony.ITelephonyRegistry$Stub.onTransact(ITelephonyRegistry.java:174)
at android.os.Binder.execTransact(Binder.java:570)
案例一:等待binder调用返回
SYS_BINDER_INFO这个文件查找当前这个线程在和谁通信
案例二:主线程等待锁
主线程的CallStack DVM thread Status是Blocked
案例三:卡在IO上
这种情况一般是和文件操作相关,判断是否是这种情况。IO占比很高,这个时候就需要查看trace日志看当时的callstack,着重看有没有file相关的动作。
案例四:主线程作耗时的动作
耗时操作造成主线程堵塞
案例五:binder线程被占满
系统对每个process最多分配15个binder线程,这个是谷歌的设计(/frameworks/native/libs/binder/ProcessState.cpp)
如果另一个process发送太多重复binder请求,那么就会导致接收端binder线程被占满,从而处理不了其它的binder请求
这本身就是系统的一个限制,如果应用未按照系统的要求来实现对应逻辑,那么就会造成问题。
而系统端是不会(也不建议)通过修改系统行为来兼容应用逻辑,否则更容易造成其它根据系统需求正常编写的应用反而出现不可预料的问题。
判断Binder是否用完,可以在trace中搜索关键字"binder_f",如果搜索到则表示已经用完,然后就要找log其他地方看是谁一直在消耗binder或者是有死锁发生,对于binder用完的前期思路大致如此。
案例六:JE或者NE导致ANR
案例七:只存在于Monkey测试下
只在Monkey环境下才能跑出来,而user版本是不会出现的,这种问题没有改动的意义。另外SAT
由于STK应用的特殊性,跑monkey会引发一些问题,如下为MTK的解释:
[DESCRIPTION]
Monkey测试时,测试SAT会经常发生 ANR 或者phone进程挂掉的问题。有时虽然没有跑到SAT中做测试,仍会因SAT应用导致许多异常现象。
[SOLUTION]
因为monkey测试时根本不适合跑SAT应用。主要是SAT应用涉及到卡跟手机或者卡跟网络的交互,这些交互过程并无固定的时间长短,且都是需要耗费一定时间。
因此,monkey测试时如果包含或者跑SAT应用就很容易发生问题(比如很常见的ANR和一些其他问题)。
建议: 不同于其他 APP,不建议monkey测试包含 SAT 应用和测试SAT应用。
小结:
总的说来,拿到一份eng的完整日志,一般步骤如下:
1.find ANR time,PID,ANR type
2.check CPU usage and Memory
3.check trace.txt,mapping process ID and time stamp
4.find more information about main thread through main log and event log
附录:
trace解析
该文档我们主要说明trace的解析和一些客观数据的如何获取。
trace它是分析死机的非常重要的手段,我们可以快速的知道,对应的process/thread 在当时正在执行哪些动作,卡住哪里等。可以非常直观的分析死机现场。
如果现场有问题机的话,我们还可以通过一些客观的执行环境来分析,通常包括如CPU 利用率,Memory 使用情况, Storage 剩余情况等。这些资料也非常重要,比如可以快速的知道,当时是否有Process 在疯狂的执行,当时是不是处于严重的low memory 情况, Storage 是否有耗尽的情况发生等。
我们知道出现问题java bt会在data/anr 保存相关的bt 信息,所以data/anr 里面的数据针对分析问题是很关键的,正常执行adb pull data/anr 我们都可以获取一些比较完整的trace信息。下面是一小段system server 的java trace的开始,从第一行开始我们来按每一行解析其所代表的含义。
1,代表 PID at time
2,然后接着Cmd line: process name
3,固定头,指明下面都是当前运行的dvm thread ,“DALVIK THREADS:” 及所包含的线程数 106
4,"main" prio=5 tid=1 Native
分别代表thread name, java thread Priority, DVM thread id, DVM thread status
"main" -> main thread -> activity thread
prio :java thread priority default is 5, (正常区域是1-10)
tid:是DVM thread id, 不是 linux thread id(下一行的sysTid才是)
Native:DVM thread Status 正常有这些状态(ZOMBIE, RUNNABLE, TIMED_WAIT, MONITOR, WAIT, INITALIZING,STARTING, NATIVE, VMWAIT, SUSPENDED,UNKNOWN)
5,group="main" sCount=1 dsCount=0 obj=0x75720fb8 self=0x7f7e8af800
代表 DVM thread status。
group:是线程所处的线程组 default is “main”
sCount: 线程被正常挂起的次数 1 (thread suspend count)
dsCount: 线程因调试而挂起次数 0 (thread dbg suspend count)
obj: 当前线程所关联的java线程对象 0x75720fb8 (thread obj address)
Sef: 该线程本身的地址 0x7f7e8af800 (thread point address)
6,sysTid=775 nice=-2 cgrp=default sched=0/0 handle=0x7f827d5e58
代表Linux thread status显示线程调度信息
sysTId: linux系统下得本地线程idlinux thread tid
Nice:线程的调度有优先级 linux thread nice value
cgrp: 优先组属 c group
sched: 调度策略 cgroup policy/gourp id
handle: 处理函数地址 handle address
7,state=S schedstat=( 225588889412 17277569839 97525 ) utm=20901 stm=1657 core=3 HZ=100
代表CPU Sched stat显示更多该线程当前上下文
State:调度状态 process/thread state (正常有 "R (running)", "S (sleeping)", "D (disk sleep)", "T (stopped)", "t (tracing stop)", "Z (zombie)", "X (dead)", "x (dead)", "K (wakekill)", "W (waking)",),通常一般的Process 处于的状态都是S (sleeping), 而如果一旦发现处于如D (disk sleep), T (stopped), Z (zombie) 等就要认真审查.
Schedstat (Run CPU Clock/ns, Wait CPU Clock/ns, Slice times) 该线程运行信息
utm: utime, user space time 线程用户态下使用的时间值(单位是jiffies)
stm: stime, kernel space time 内核态下得调度时间值
core: now running in cpu. 最后运行改线程的cup标识
8,stack=0x7f7dc93000-0x7f7dc95000 stackSize=1020KB
代表堆栈地址区域及size
9,held mutexes=
代表是否被锁住,正常有四个属性(mutexes: tll=0 tsl=0 tscl=0 ghl=0),0表示unlock,其它值都代表被lock,
tll: thread List Lock,
tsl: thread Suspend Lock,
tscl: thread Suspend Count Lock
ghl: gc Heap Lock
10,剩余的就是一些 Call Stack
CPU Usage
追查CPU 利用率可大体的知道,当时机器是否有Process 在疯狂的运行, 当时系统运行是否繁忙。通常死机分析,只需要抓取基本的使用情况即可。通常使用的命令如 top
top 可以简单的查询Cpu 的基本使用情况,注意的是top 的CPU% 是按全部CPU 来计算的,如果以单线程来计算,比如当时有开启4个核心,那么最多吃到25%.
Memory Usage
我们通常会审查,系统当时memory 是否足够, 是否处于low memory 状态, 是否可能出现因无法申请到memory 而卡死的情况. 常见的一些基本信息如下:
* meminfo: basic memory status
adb shell cat proc/meminfo
adb shell cat proc/pid/maps
adb shell cat proc/pid/smaps
* procrank info: all process memory status
adb shell procrank
adb shell procmem pid
adb shell dumpsys meminfo pid
adb shell cat proc
adb shell cat /proc/buddyinfo
Storage Usage
查看Storage 的情况,通常主要是查询data 分区是否已经刷满, sdcard 是否已经刷满, 剩余的空间是否足够。以及是否有产生超大文件等。 通常使用的命令如 df
2.5.5 android默认分析工具
android_main.txt、/data/anr/traces.txt
2.6 小结
MTK参考设计交付致力于较好的用户体验,尽管如此,在以下情况下系统可能出现性能问题:
1、使用不同的硬件元件,如:触摸屏、相机传感器等,以及驱动尚未优化;
2、修改默认的应用行为;
3、安装一些自己的应用程序或第三方的应用程序。
可以利用现有的性能工具来识别问题。
出现异常时,可以使用top命令、ps命令、renice命令等查看当时的状态和统计信息。
Oprofile、Ftrace、Traceview工具
3、开机速度优化
3.1 开机时间评估
硬件环境:1、不要拔电池关机;2、定义好开始时间和结束时间,测试3~5次,取平均值。
软件环境:1、第三方应用安装的数量;2、应用列表的补丁;其他修改(内核、驱动、系统框架)
在优化开机时间之前,需要获取开机每个阶段的时间,然后找到影响速度的瓶颈。影响开机速度的各个阶段如下所示:
1、内核初始化时间:几乎是固定不变的时间
2、挂载分区的总时间:几乎是固定不变的时间,若拔电池再开机的话这个阶段时间会增加
3、Android启动时间:zygote预加载类(几乎是固定不变的时间);包扫描(验证每个apk扫描安装的时间);AP初始化时间(取决于安装的apk)
3.2 开机时间分析
注意:预加载和lk启动时间仅仅在android 4.1版本中启用。
方法:cat /proc/bootprof
3.3 开机时间优化
为了优化开机时间,必须要知道哪些进程比较耗时。可以通过整理每个进程cpu的时间从系统服务开启到关机动画结束(桌面图标呈现出来)。
使用方法:获取文件节点的值:/proc/mtprof/cputime
cat/proc/mtprof/cputime
1、阻止不必要的ap应用进程启动;(应用进程被定义为常驻;通过服务请求、content Provider、广播拉起)
2、开机动画
开机动画比较耗时的任务:动画越多cpu占用的时间越长;每秒传输的帧数越多,cpu越耗时。
3.4 odex预加载配置
为了减小开机启动时间,可以选择在编译期间把部分APK odex 打包到syetem.img.在首次启动时不需要进行APK odex 优化。
4、应用启动速度优化
MTK工具:系统层次(内核—ftrace、cpu占用时间)、AP层次(traceview) GAT tools.
4.1 应用启动方式
通常来说,在安卓中应用的启动方式分为两种:冷启动和热启动。
1、冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
2、热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。
特点
1、冷启动:冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
2、热启动:热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application,因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。
上面说的启动是点击app的启动图标来启动的,而另外一种方式是进入最近使用的列表界面来启动应用,这种不应该叫启动,应该叫恢复。
4.2 应用启动流程
在安卓系统上,应用在没有进程的情况下,应用的启动都是这样一个流程:当点击app的启动图标时,安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类、创建MainActivity类、加载主题样式Theme中的windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了后最后才进行contentView的measure/layout/draw显示在界面上,所以直到这里,应用的第一次启动才算完成,这时候我们看到的界面也就是所说的第一帧。
所以,总结一下,应用的启动流程如下:
Application的构造器方法——>attachBaseContext()——>onCreate()——>Activity的构造方法——>onCreate()——>配置主题中背景等属性——>onStart()——>onResume()——>测量布局绘制显示在界面上。
测量应用启动的时间
在上面这个启动流程中,任何一个地方有耗时操作都会拖慢我们应用的启动速度,而应用启动时间是用毫秒度量的,对于毫秒级别的快慢度量我们还是需要去精确的测量到到底应用启动花了多少时间,而根据这个时间来做衡量。
什么才是应用的启动时间
从点击应用的启动图标开始创建出一个新的进程直到我们看到了界面的第一帧,这段时间就是应用的启动时间。
我们要测量的也就是这段时间,测量这段时间可以通过adb shell命令的方式进行测量,这种方法测量的最为精确,命令为:
<code class="hljs sql has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">adb shell am <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">start</span> -W [packageName]/[packageName.MainActivity]</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
执行成功后将返回三个测量到的时间:
1、ThisTime:一般和TotalTime时间一样,除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。
2、TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。
3、WaitTime:一般比TotalTime大点,包括系统影响的耗时。
下面是测量一个应用冷启动和热启动的时间:
可以看到在进程已经存在的情况下,只需要重新初始化MainActivity,这样的启动比较快,不过大多数情况下应用的启动都是冷启动,因为用户都会在任务列表中手动关闭遗留的应用进程。
4.3减少应用启动时的耗时
针对冷启动时候的一些耗时,如上测得这个应用算是中型的app,在冷启动的时候耗时已经快700ms了,如果项目再大点在Application中配置了更多的初始化操作,这样将可能达到1s,这样每次启动都明显感觉延迟,所以在进行应用初始化的时候采取以下策略:
1、在Application的构造器方法、attachBaseContext()、onCreate()方法中不要进行耗时操作的初始化,一些数据预取放在异步线程中,可以采取Callable实现。
2、对于sp的初始化,因为sp的特性在初始化时候会对数据全部读出来存在内存中,所以这个初始化放在主线程中不合适,反而会延迟应用的启动速度,对于这个还是需要放在异步线程中处理。
3、对于MainActivity,由于在获取到第一帧前,需要对contentView进行测量布局绘制操作,尽量减少布局的层次,考虑StubView的延迟加载策略,当然在onCreate、onStart、onResume方法中避免做耗时操作。
遵循上面三种策略可明显提高app启动速度。
4.4优化应用启动时的体验
对于应用的启动时间,只能是尽量的避免一些耗时的、非必要的操作在主线程中,这样相对可以缩减一部分启动的耗时,另外一方面在等待第一帧显示的时间里,可以加入一些配置以增加体验,比如加入Activity的background,这个背景会在显示第一帧前提前显示在界面上。
1、先为主界面单独写一个主题style,设置一张待显示的图片,这里我设置了一个颜色,然后在manifest中设置给MainActivity:
<code class="hljs xml has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">style</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">name</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"AppTheme.Launcher"</span>></span><span class="css" style="box-sizing: border-box;"> <<span class="hljs-tag" style="color: rgb(0, 0, 0); box-sizing: border-box;">item</span> <span class="hljs-tag" style="color: rgb(0, 0, 0); box-sizing: border-box;">name</span>="<span class="hljs-tag" style="color: rgb(0, 0, 0); box-sizing: border-box;">android</span><span class="hljs-pseudo" style="color: rgb(0, 0, 0); box-sizing: border-box;">:windowBackground"</span>><span class="hljs-at_rule" style="box-sizing: border-box;">@<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">drawable/bule</item></span> </span></span><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">style</span>></span> //... <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">activity </span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:name</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">".MainActivity"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:label</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"@string/app_name"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:theme</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"@style/AppTheme.Launcher"</span>></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">intent-filter</span>></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">action</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:name</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"android.intent.action.MAIN"</span> /></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">category</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:name</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"android.intent.category.LAUNCHER"</span> /></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">intent-filter</span>></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">activity</span>></span> </code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li></ul>
2、然后在MainActivity中加载布局前把AppTheme重新设置给MainActivity:
<code class="hljs java has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onCreate</span>(Bundle savedInstanceState) { setTheme(R.style.AppTheme); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } </code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>
这样在启动时会先显示background,然后待界面绘制完成再显示主界面