System Trace
Hierarchy Viewer
TraceView
冷启动:耗时最多,是启动优化的衡量标准。启动应用时,后台没有该应用的进程,系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
热启动:应用从后台到前台
优化方向:主要针对冷启动,Application和Activity生命周期
系统提供的命令:(线下使用方面,不能带到线上;非严谨精确的时间)
adb shell am start -S -W 包名/启动类的全限定名 //-S 表示重启当前应用
执行
C:\Android\Demo>adb shell am start -S -W com.example.moneyqian.demo/com.example.moneyqian.demo.MainActivity
Stopping: com.example.moneyqian.demo
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.moneyqian.demo/.MainActivity }
Status: ok
Activity: com.example.moneyqian.demo/.MainActivity
ThisTime: 2247
TotalTime: 2247
WaitTime: 2278
Complete
ThisTime : 最后一个Activity的启动耗时(例如从 LaunchActivity - >MainActivity「adb命令输入的Activity」 , 只统计 MainActivity 的启动耗时)
TotalTime : 所有activity的启动耗时(有几个Activity 就统计几个)
WaitTime : 应用进程的创建过程 + TotalTime (AMS启动activity的总耗时)
手动打点:(精确,可带到线上,推荐)
注意截止时间的选取
误区:onWIndowFocusChanged 首帧时间
正解:真实数据展示,Feed第一条展示
1.TraceView:
特点:
运行时开销严重
可能会带偏优化方向
TraceView使用方式
代码中添加:
Debug.startMethodTracing();
被检测的方法;
Debug.stopMethodTracing();
需要使用adb pull将生成的.trace文件导出到电脑,然后使用Android Studio的Profiler进行加载;
打开 Profiler -> CPU -> 点击 Record -> 点击 Stop -> 查看Profiler下方Top Down/Bottom Up 区域,以找出耗时的热点方法。
结合CPU Profiler 分析
使用 Profile 的 CPU 模块可以帮我们快速找到耗时的热点方法。
TraceView作用:主要做热点分析,用来得到以下两种数据:
单次执行最耗时的方法。
执行次数最多的方法。
2.System Trace:
集合android内核的数据,生成html报告
API18以上使用,推荐TraceCompat
轻量级,开销小
直观反应cpu利用率
cputime与walltime的区别
- cputime是代码消耗cpu的时间(重点指标,优化方向)
- walltime是代码执行的时间(举例:锁冲突可能导致walltime时间长很多)
作用:
主要用于分析绘制性能方面的问题。
分析系统关键方法和应用方法耗时。
启动白屏优化—>theme切换:感觉上的快,对实际启动速度无帮助
解决启动白屏问题
再启动app时展示一张静态图 ,实现方法是给splash页面设置theme主题 ,主题设置background背景图代替白屏页,展示完成背景图和splash之后,,在Mainactivity onCreat()之前把系统appTheme设置回来。
核心思想:子线程分担主线程的任务,并行减少时间
Application中加入异步线程:把application中各种初始化操作分散到子线程中执行。
把各种初始化的操作,通过线程池,放到子线程中执行,这里创建线程池的核心线程数,参考的是asyncTask中的核心线程数量的设置。如果自己随便设置,可能超过CPU的核数或者低于CPU核数太多。
这样处理后,application的onCreate将会快很多,但是会出现一些问题,比如有些初始化操作在子线程中还没执行完,其它地方如splashActivity已经要开始使用该方法中的某些内容。就会报错。那么要么放弃把这个初始化方法放到子线程,要么参考下面的方案
解决方案:通过CountDownLatch,在application中创建一个CountDownLatch
//相当于自己加一个锁
private final CountDownLatch countDownLatch=new CountDownLatch(1);
...
@Override
public void onCreate() {
...
service.submit(new Runnable() {
@Override
public void run() {
initGeTui();
countDownLatch.countDown(); //CountDown满足一次
}
});
try {
countDownLatch.await(); //会检测执行次数是否满足
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这样,只有当countDown执行完,await()方法才会执行完,相当于application的onCreate才执行完
上面方案的缺点:
启动器的核心思想:充分利用CPU多核,自动梳理任务顺序
启动器设计流程:
结合IdleHandler,在空闲时执行
提前加载SharedPreferences:
Multidex之前加载,利用此阶段CPU
启动阶段不启动子进程:
子进程会共享CPU资源,导致主进程CPU紧张
类加载优化:
提前异步类加载(使用的时候不用再加载)
启动阶段抑制GC
CPU锁频
1.你做启动优化怎么做的?
分析现状,确认问题。–目前的启动流程已经非常复杂,主线程执行了太多的代码。
异步优化。–异步初始化–>启动器
延迟初始化。–有些代码可能不需要application中执行,可以在页面初始化后空闲时
2.是怎么异步的,异步遇到什么问题没有?
new thread 或者线程池
代码变得不够优雅,依赖关系不好处理—>启动器
3.启动优化,有哪些容易忽略的注意点?
cpu time和wall time的区别
注意延迟初始化的优化
介绍下黑科技
4.版本迭代导致的启动变慢有什么好的解决办法吗?
启动器
结合CI
监控完善
工具:system trace
关注frames
正常:绿色原点,丢帧:黄色或红色
Alerts栏
工具:Layout Insepector
AndroidStudio自带工具,可查看视图层次结构
减少布局嵌套:层级嵌套过深的话,深度遍历各个节点会非常消耗时间,这也是布局优化余地最大的一个点了。使用约束布局 Constraintlayout减少嵌套 。
使用合适的布局:
简单布局 FrameLayout>LinearLayout>RelativeLayout;
复杂布局 RelativeLayout>LinearLayout>FrameLayout
如果用 RelativeLayout 可以避免布局嵌套的话是值得的。(LinearLayout在设置weight时,也会调用子View 2次 onMeasure)
使用 include 标签: 在布局文件中,标签可以指定插入一段布局文件到当前布局。这样的话既提高了布局复用,也减少了我们的代码书写。另外,merge标签可以和include的标签一起使用从而减少布局层级。
ViewStub 延时加载: 有些布局,比如网络出错的布局,没必要在所有时候都加载出来。使用 ViewStub 可以实现按需加载。ViewStub 本身没有宽高,加载起来几乎不消耗什么资源。当对他setVisibility(View.VISIBLE)的时候会调用它引用的真实布局填充到当前位置,从而实现了延时加载,节省了正常加载的时间。
避免过渡绘制:去掉多余的背景色,减少复杂shape的使用
ondraw中避免:创建大对象,耗时操作
分时调度模型:让所有线程轮流获取 CPU 的使用权,而且均分每个线程占用 CPU 的时间片,这种方式非常公平
抢占式调度模型:让优先级高的线程优先获取到 CPU 的使用权,如果在可运行池当中的线程优先级都一样,那就随机选取一个(JVM使用这种)
如何锁定线程创建者:
项目变大之后收敛线程;项目源码、三方库、arr中都有线程的创建
分析:
创建线程的位置获取堆栈,
所有的异步方式,都会走到new thread
怎么在项目中对线程进行优化
线程收敛
统一线程池:任务区分
监控重要异步任务
线程的创建和销毁会带来比较大的性能开销。因此线程优化也很有必要。查看项目中是否存在随意 new thread,线程缺乏管理的情况。使用 AsyncTask 或者线程池对线程进行管理,可以提升 APP 的性能。另外,我比较推荐使用 Rxjava 来实现异步操作,既方便又优雅。
连接复用:节省连接建立时间,如开启 keep-alive。于Android来说默认情况下HttpURLConnection和HttpClient都开启了keep-alive。
请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的CSS Image Sprites。同一个页面数据尽量放到一个接口中去处理。
减少请求数据的大小:对于post请求,body可以做gzip压缩的,header也可以做数据压缩(不过只支持http 2.0)。 返回数据的body也可以做gzip压缩,body数据体积可以缩小到原来的30%左右(也可以考虑压缩返回的json数据的key数据的体积,尤其是针对返回数据格式变化不大的情况,支付宝聊天返回的数据用到了)。
根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)。
使用HttpDNS优化DNS:DNS存在解析慢和DNS劫持等问题,DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。
大量数据的加载采用分页的方式;上传图片时,在必要的时候压缩图片。
网络优化工具:Network Profiler
1.显示实时网络波动:发送、接收数据及连接数
2.需要启动高级分析
3.只支持HttpURLConnection和OkHttp库
抓包工具:Charles(使用java开发的)
断点功能
Map Local
弱网环境模拟
NetWorkStatsManager
获取线上使用的流量情况
前后台流量获取方案
在网络方面你做了哪些监控,建立了哪些指标?
初期没有做,开发都是连着wifi,没有注意到这个问题。用户量增多后,收到用户的反馈。
补上网络方面的监控:
质量:请求成功率,每步耗时,状态码
流量:精确统计、前后台流量消耗
如何有效的降低用户流量消耗?
数据:缓存、增量更新
上传:压缩
图片:缩略图、webp
用户反馈消耗流量多这种问题怎么查?
精准流量获取能力
所有请求大小及次数的监控
主动预警能力
Apk 组成结构:
1.三方库处理:
2.资源优化
图片只保留一份,如xhdpi
android自带的功能:移除无用资源
jpg/png转webp
优先考虑能否用 shape 代码、.9 、svg、VectorDrawable 类来替换传统的图片
避免使用帧动画,可使用 Lottie 动画库
3.代码优化
启用混淆以移除无用代码(minifyEnabled true )
开启代码压缩( minifyEnabled true //打开代码压缩)
剔除 R 文件
用注解替代枚举
4.arsc文件优化
移除未使用的备用资源来优化 .arsc 文件
android {
defaultConfig {
...
resConfigs "zh", "zh_CN", "zh_HK", "en"
}
}
5.so库打包优化(此种优化效果非常明显)
so是Android上的动态链接库,七种不同类型的CPU架构:armeabi、armeabi-v7a、arm64-v8a、x86
、x86_64、MIPS、MIPS64。arm 系列是绝大多数手机上使用的,x86 主要是运用在平板上。
优化方式一:移出非主流的so库,只提供对主流架构的支持,一般选择armeabi-v7a。
android {
defaultConfig {
...
ndk {
abiFilters "armeabi-v7a"
}
}
}
这种方案相对来说比较暴力。
优化方案二(更优方案)
完美支持所有设备类型代价太大。都放在armeabi目录,根据CPU类型加载对应架构so。
其它方案:
首先需要了解ava 内存回收机制——GC机制,Java 对象引用方式 —— 强引用、软引用、弱引用和虚引用。
基础知识:
1.针对进程的内存策略
进程的内存分配策略为:由 ActivityManagerService 集中管理所有进程的内存分配。
进程的内存回收策略为:首先Application Framework 决定回收的类型,当进程的内存空间紧张时会按照进程优先级由低到高的顺序自动回收进程及内存。
Android将进程分为5个优先级,具体如下:
真正执行回收进程的操作的是 Linux 内核。
梳理一下整体流程:
ActivityManagerService 对所有进程进行评分。
更新评分到 Linux 内核。
由 Linux 内核完成真正的内存回收。
2.针对对象、变量的内存策略
Android的对于对象、变量的内存策略同 Java
内存管理 = 对象 / 变量的内存分配 + 内存释放
内存分配策略
对象,变量的内存分配有系统负责,共有三种:静态分配、栈式分配、堆式分配,分别面向静态变量,动态变量和对象实例。
内存释放策略
对象,变量的内存释放由Java的垃圾回收器GC负责。
内存分配注意:(非常重要)
成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)—因为他们属于类,类对象最终还是要被new出来的。
局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。—–因为他们属于方法当中的变量,生命周期会随着方法一起结束。
public class Sample {
// 该类的实例对象的成员变量s1、mSample1及指向的对象都存放在堆内存中
int s1 = 0;
Sample mSample1 = new Sample();
// 方法中的局部变量s2、mSample2存放在 栈内存
// 变量mSample2所指向的对象实例存放在 堆内存
public void method() {
int s2 = 0;
Sample mSample2 = new Sample();
}
}
// 变量mSample3的引用存放在栈内存中
// 变量mSample3所指向的对象实例存放在堆内存
// 该实例的成员变量s1、mSample1也存放在堆内存中
Sample mSample3 = new Sample();
内存泄露
即 ML (Memory Leak),指 程序在申请内存后,当该内存不需再使用但却无法被释放,归还给 程序的现象。对应用程序的影响:容易使得应用程序发生内存溢出,即OOM(out of Memory)
发生内存泄露的本质原因:
本质原因:持有引用者的生命周期>被引用者的生命周期
解释:本该回收的对象(该对象已经不再被使用),由于某些原因(如被另一个正在使用的对象引用)不能被回收。
内存抖动
优化方案
尽量避免频繁创建大量、临时的小对象
1)使用更加轻量的数据结构
例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构,下图演示了HashMap的简要工作原理,相比起Android系统专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。
可以参考Android性能优化典范 - 第3季
2)避免在Android里面使用Enum
3)减小Bitmap对象的内存占用
Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用是很重要的,通常来说有下面2个措施:
inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
4)使用更小的图片
在设计给到资源图片的时候,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用一张更小的图片。尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的InflationException。
1)复用系统自带的资源
Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异,不符合需求的情况,还是需要应用程序自身内置进去。
2)注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用
3)Bitmap对象的复用
在ListView与GridView等显示大量图片的控件里面需要使用LRU的机制来缓存处理好的Bitmap。
4)避免在onDraw方法里面执行对象的创建
类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。
5)StringBuilder
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。
内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,gc就会更容易被触发,容易出现内存抖动,从而引起性能问题。(LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况)
1)注意Activity的泄漏
通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的Activity泄漏:
内部类引用导致Activity的泄漏:非静态(匿名)内部类会默认持有外部类引用。
Handler导致的Activity泄漏(最经典的场景),如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象(removeCallbacksAndMessages(null)–同时清空消息队列 ,结束Handler生命周期)。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。为了保证Handler中消息队列中的所有消息都能被执行,此处推荐使用 静态内部类 + 弱引用的方式。
线程造成的内存泄漏
在 Activity 内定义了一个匿名的 AsyncTask 对象,就有可能发生内存泄漏。如果 Activity 被销毁之后 AsyncTask 仍然在执行,那就会阻止垃圾回收器回收Activity 对象,进而导致内存泄漏,直到执行结束才能回收 Activity。
同样的,使用 Thread 和 TimerTask 也可能导致 Activity 泄漏。只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 Activity 的强引用,进而导致内存泄漏。
总结:
内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意!我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。
2)考虑使用Application Context而不是Activity Context
对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。(Activity Context被传递到其他实例中,这可能导致Activity自身被引用而发生泄漏)
3)注意临时Bitmap对象的及时回收
4)注意监听器的注销
在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。
5)注意WebView的泄漏
Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,看这里WebView causes memory leak - leaks the parent Activity。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
6)注意Cursor对象是否及时关闭
在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。
(1)Memory Monitor 工具:
它是Android Studio自带的一个内存监视工具,它可以很好地帮助我们进行内存实时分析。通过点击Android Studio右下角的Memory Monitor标签,打开工具可以看见较浅蓝色代表free的内存,而深色的部分代表使用的内存从内存变换的走势图变换,可以判断关于内存的使用状态,例如当内存持续增高时,可能发生内存泄漏;当内存突然减少时,可能发生GC等,如下图所示。
(2)LeakCanary工具:
LeakCanary是Square公司基于MAT开发的一款监控Android内存泄漏的开源框架。其工作的原理是: 监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收(在ReferenceQueue中说明可以被回收,不存在泄漏;否则,可能存在泄漏,LeakCanary是执行一遍GC,若还未在ReferenceQueue中,就会认定为泄漏)。
如果Activity被认定为泄露了,就抓取内存dump文件(Debug.dumpHprofData);之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析;接着通过HeapAnalyzer (checkForLeak—findLeakingReference—findLeakTrace)来进行内存泄漏分析。最后通过DisplayLeakService进行内存泄漏的展示。
(3)Android Lint 工具:
Android Lint Tool 是Android Sutido种集成的一个Android代码提示工具,它可以给你布局、代码提供非常强大的帮助。硬编码会提示以级别警告,例如:在布局文件中写了三层冗余的LinearLayout布局、直接在TextView中写要显示的文字、字体大小使用dp而不是sp为单位,就会在编辑器右边看到提示。
电量重视度不够:开发中长时间连着手机,难以感知到电量变化
电量消耗线上难以量化
如何解决问题:
1、找特定场景专项测试:比如进入详情页操作一段时间,再看电量变化。
2、注册电量相关的广播:ACTION_BATTERY_CHANGED
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
mBatteryLowReceiver = new BatteryLowReceiver();
registerReceiver(mBatteryLowReceiver,intentFilter);
Battery Historian(电量使用记录分析工具)
Battery Historian是Android 5.0开始引入的新API。通过下面的指令,可以得到设备上的电量消耗信息。
特点:
使用步骤:
面试问题模拟
1.怎么做电量测试?
电量相关的测试相对难做一些,因为app在具体用户的耗电量无法统计,每个设备的硬件不一样,相关的功耗也不一样,且功耗值只能在线下拿到。只能尽可能监控来判断
分场景逐个击破:按照app功能进行针对性的专项测试,利用手机设置里面的电量消耗功能作为判断的依据,操作某个功能一段时间之后,看耗电量,直观,但精确性不行
Battery Historian : google推出的Android系统电量分析工具,5.0以后可用。拿到的电量信息精确也丰富很多。可以获取到详细的耗电组件:GPS、weakLock、蓝牙等的工作时间及耗电量。 可以比对优化前及优化后的电量信息。
但是这个工具只能在线下使用,因此需要线下测试后增加一些线上监控,比如耗电组件的使用次数,调用堆栈以及访问时间, 如果有用户在线上反馈,就可以通过这些信息来判断用户是否有耗电的操作。
2.有哪些有效的电量优化手段?
因为不能精确的统计线上电量消耗,因此需要尽可能线下优化好电量。
界面相关
离开界面后停止相关活动
耗电操作判断前台后台,如果后台就不操作(如动画,onresume中start onpause中cancel)
网络相关:
控制网络请求时机和次数;将可以延迟的网络请求批量发送
网络数据传输前进行数据压缩,减少时间,还能节约流量
禁止使用轮训方式做业务操作
不需要实时性的任务在连接wifi后在执行(wifi网络传输的电量消耗要比移动网络少很多)
定位相关(传感器相关)
根据场景谨慎选择定位模式
考虑网络定位代替GPS
使用后务必及时关闭,减少更新频率
WakeLock 相关
注意成对出现:acquire和release
使用带参数的acquire来设置超时时间,避免异常情况导致WakeLock无法释放
finally确保一定会被释放
JobScheduler