8.电量优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//电量优化
1.电量优化检测方案:
Battery Historian
- Google推出的一款Android系统电量分析工具
- 支持50(API21)及以上系统的电量分析
- 功能强大,推荐使用
- 可视化展示指标:耗电比例、执行时间、次数
- 适合线下使用
2.电量优化套路总结:
1.网络相关:控制请求时机及次数,数据压缩,减少时间
2.传感器相关:
1.根据场景谨慎选择定位模式
2.考虑网络定位代替GPS
3.使用后务必及时关闭,减少更新频率

一、电量优化介绍及方案选择

正确认识

1
2
1.电量重视度不够:开发中一直连接手机
2.电量消耗线上难以量化

方案介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
设置->耗电排行
- 直观,但没有详细数据,对解决问题没有太多帮助
- 找特定场景专项测试
ACTION BATTERY CHANGED
- 获取电池电量、充电状态、电池状态等信息
- 示例:
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
Intent intent = registerReceiver(null, filter);
LogUtils.i("battery " + intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1));
- 价值不大:针对手机整体的耗电量,而非特定App
Battery Historian(推荐)
- Google推出的一款Android系统电量分析工具
- 支持50(API21)及以上系统的电量分析
- 功能强大,推荐使用
- 可视化展示指标:耗电比例、执行时间、次数
- 适合线下使用

测试相关

1
2
3
1.耗电场景测试:复杂运算、视频播放
2.传感器相关: 使用时长、耗电量、发热
3.后台静默测试

二、Battery Historian实战及分析

安装

1
2
3
https://github.com/google/battery-historian
安装Docker
docker -- run -p <port>:9999 gcr.io/android-batterynistorian/stable:3.0 --port 9999

导出电量信息

1
2
3
adb shell dumpsys batterystats --reset
adb shell dumpsys batterystats --enable full-wakehistory
adb bugreport bugreport.zip

上传分析

1
2
3
http://localhost:9999
上传bugreport文件即可
备用:https://bathist.ef.lc/(需翻墙)

使用方式:代码过多百度查看,如:https://blog.csdn.net/hpc19950723/article/details/54381246

三、电量辅助监控实战

运行时能耗

1
2
adb pull /system/framework/framework-res.apk//导出apk
反编译,xml-》power_profile

image-20230921092749912

运行时获取使用时长

1
2
1.Aop辅助统计:次数、时间
2.以WakeLock为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.optimize.performance.wakelock;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.PowerManager;

public class WakeLockUtils {
private static PowerManager.WakeLock sWakeLock;
public static void acquire(Context context){
if(sWakeLock == null){
sWakeLock = createWakeLock(context);
}
if(sWakeLock != null && !sWakeLock.isHeld()){
sWakeLock.acquire();
sWakeLock.acquire(1000);
}
}
public static void release(){
// 一些逻辑
try{

}catch (Exception e){

}finally {
// 为了演示正确的使用方式
if(sWakeLock != null && sWakeLock.isHeld()){
sWakeLock.release();
sWakeLock = null;
}
}
}
@SuppressLint("InvalidWakeLockTag")
private static PowerManager.WakeLock createWakeLock(Context context){
PowerManager pm = (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE);
if(pm != null){
return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"");
}
return null;
}
}

四、电量优化套路总结

CPU时间片

1
2
1.获取运行过程线程CPU消耗,定位CPU占有率异常方法
2.减少后台应用的主动运行

网络相关

1
2
3
1.请求时机及次数控制
2.数据压缩,减少时间
3.禁止使用轮询功能

定位相关

1
2
3
1.根据场景谨慎选择定位模式
2.考虑网络定位代替GPS
3.使用后务必及时关闭,减少更新频率

界面相关

1
2
1.离开界面后停止相关活动
2.耗电操作判断前后台

WakeLock相关

1
2
3
1.注意成对出现:acquire与release
2.使用带参数的acquire
3.finally确保一定会被释放

JobScheduler

1
2
1.在符合某些条件时创建执行在后台的任务
2.把不紧急的任务放到更合适的时机批量处理

五、问题

怎么做电量测试

1
2
3
1.分场景逐个击破
2.Battery Historian
3.辅助监控

有哪些有效的电量优化手段

1
2
3
1.网络相关
2.传感器相关
3.WakeLock与JobScheduler
7.网络优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//网络优化
1.网络缓存:
1.服务端返回加上过期时间,避免每次重新获取
2.OkHttp、Retfort、Volley都有较好实践
3.无网络时展示数据缓存;
2.网络质量:
1.时长、业务成功率、失败率的统计,在OkHttp、Retfort、Volley等自行统计相关数据并在特定场景合并上报;
2.弱网络模拟
3.增量更新:
1.加上版本的概念,只传输有变化的数据
4.数据压缩:
1.Post请求Body使用GZip压缩(字符压缩)
2.图片上传之前必须压缩
5.图片相关:
1.图片使用策略细化:优先缩略图
2.使用WebP格式图片

一、网络优化从哪些纬度开展

正确认识

1
2
3
4
5
6
1.网络优化的纬度:多维
2.仅仅重视流量不够
3.网络流量的消耗量:精确
4.整体均值掩盖单点问题
5.网络相关监控:全面
6.粗粒度监控不能帮助我们发现、解决深层次问题

网络优化纬度

1
2
3
4
5
6
7
8
9
10
流量消耗
- 一段时间流量消耗的精准度量,网络类型、前后台
- 监控相关:用户流量消耗均值、异常率( 消耗多、次数多)
- 完整链路全部监控(Request、Response),主动上报
网络请求质量
- 用户体验:请求速度、成功率
- 监控相关: 请求时长、业务成功率、失败率、T失败接口
其它
- 公司成本:带宽、服务器数、CDN
- 耗电

网络优化误区

1
2
1.只关注流量消耗,忽视其它纬度
2.只关注均值、整体,忽视个体

二、网络优化工具选择

Network Profiler

1
2
3
1.显示实时网络活动:发送、接收数据及连接数
2.需要启用高级分析(androidStudio顶部-run-Edit Configurations...->勾上(Profiling-Enable advanced profiling (required for API level < 26 only))
3.只支持 HttpURLConnection和OkHttp网络库

发送一个请求后在prifoler中出现下图情况,选中请求的时间端后可以查看到请求的状态及大小等信息;image-20230920144449262

抓包工具

1
2
3
4
1.Charles
2.Fiddler
3.Wireshark
4.TcpDump

Charles使用

1
2
3
1.断点功能
2.Map Local
3.弱网环境模拟

启动软件后发送请求,会自动抓包image-20230920144728904

Stetho

1
2
强大的应用调试桥,连接Android和Chrome
网络监控、视图查看、数据库查看、命令行扩展等

Stetho使用

1
2
3
4
1.com.facebook.stetho:stetho-okhttp3:1.5.0
2.Stetho.initializeWithDefaults(this):
3.addNetworkInterceptor
4.Chrome浏览器:chrome://inspect(需翻墙)

三、精准获取流量消耗实战

问题思考

1
2
3
4
如何判断App流量消耗偏高
- 绝对值看不出高低
- 对比竞品,相同Case对比流量消耗
- 异常监控超过正常指标(线上)

测试方案

1
2
3
1.设置-流量管理
2.抓包工具:只允许本App联网
3.可以解决大多数问题,但是线上场景线下可能遇不到

线上流量获取方案

1
2
3
4
5
6
7
8
9
10
TrafficStats:API8以上重启以来的流量数据统计
- TrafficStats:API8以上重启以来的流量数据统计
- getUidRxBytes(int uid)指定Uid的接收流量
- getTotalTxBytes0)总发送流量
缺点:
- 无法获取某个时间段内的流量消耗
NetworkStatsManager:API23之后流量统计(推荐)
- 可获取指定时间间隔内的流量信息
- 可获取不同网络类型下的消耗
- 使用示例查看百度

前后台流量获取方案

1
2
3
1.难题:线上反馈App后台跑流量
2.只获取一个时间段的值不够全面
后台定时任务->获取间隔内流量->记录前后台->分别计算->上报APM后台->流量治理依据

前后台流量获取方案总结

1
2
1.有一定误差,可接受范围内
2.结合精细化的流量异常报警针对性的解决后台跑流量

四、网络请求流量优化实战

使用网络的场景概述

1
2
3
数据:Api、资源包(升级包、H5、RN)、配置信息
图片:下载、上传
监控:APM相关、单点问题相关

数据缓存

1
2
3
1.服务端返回加上过期时间,避免每次重新获取
2.节约流量且大幅提高数据访问速度,更好的用户体验
3.OkHttp、Volley都有较好实践

无网络时使用缓存数据示例,不同框架不一样,只做参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.optimize.performance.net;

import com.optimize.performance.PerformanceApp;
import com.optimize.performance.utils.Utils;
import java.io.IOException;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class NoNetInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder();
if(!Utils.isNetworkConnected(PerformanceApp.getApplication())){
builder.cacheControl(CacheControl.FORCE_CACHE);
}
return chain.proceed(builder.build());
}
}

package com.optimize.performance.net;
import com.optimize.performance.PerformanceApp;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.fastjson.FastJsonConverterFactory;
public class RetrofitNewsUtils {
private static final APIService API_SERVICE;

public static APIService getApiService() {
return API_SERVICE;
}

public static final String HTTP_SPORTSNBA_QQ_COM = "http://sportsnba.qq.com/";

static {
OkHttpClient.Builder client = new OkHttpClient.Builder();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
Cache cache = new Cache(PerformanceApp.getApplication().getCacheDir(),10*1024*1024);
client.
eventListenerFactory(OkHttpEventListener.FACTORY).
dns(OkHttpDNS.getIns(PerformanceApp.getApplication())).
addInterceptor(new NoNetInterceptor()).
addInterceptor(logging);

final Retrofit RETROFIT = new Retrofit.Builder()
.baseUrl(HTTP_SPORTSNBA_QQ_COM)
.addConverterFactory(FastJsonConverterFactory.create())
.client(client.build())
.build();
API_SERVICE = RETROFIT.create(APIService.class);
}
}

增量数据更新

1
2
1.加上版本的概念,只传输有变化的数据
2.配置信息、省市区县等更新

数据压缩

1
2
3
1.Post请求Body使用GZip压缩
2.请求头压缩
3.图片上传之前必须压缩

优化发送频率和时机

1
2
1.合并网络请求、减少请求次数
2.性能日志上报:批量+特定场景上报

图片相关

1
2
1.图片使用策略细化:优先缩略图
2.使用WebP格式图片

总结

1
2
数据缓存、增量更新、压缩、图片相关等
需要结合实际情况进行选择

五、网络请求质量优化实战

质量指标

1
2
1.网络请求成功率
2.网络请求速度

Http请求过程

1
2
3
1.请求到达运营商的Dns服务器并解析成对应的IP地址
2.创建连接,根据IP地址找到相应的服务器,发起一个请求
3.服务器找到对应的资源原路返回访问的用户

DNS相关

1
2
3
问题:DNS被劫持、DNS解析慢
方案:使用HttpDNS,绕过运营商域名解析过程
优势:降低平均访问时长、提高连接成功率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//示例:
package com.optimize.performance.net;
import android.content.Context;
import com.alibaba.sdk.android.httpdns.HttpDns;
import com.alibaba.sdk.android.httpdns.HttpDnsService;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import okhttp3.Dns;

public class OkHttpDNS implements Dns {

private HttpDnsService dnsService;
private static OkHttpDNS instance = null;
private OkHttpDNS(Context context) {
dnsService = HttpDns.getService(context, "");
}
public static OkHttpDNS getIns(Context context) {
if (instance == null) {
synchronized (OkHttpDNS.class) {
if (instance == null) {
instance = new OkHttpDNS(context);
}
}
}
return instance;
}

@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
String ip = dnsService.getIpByHostAsync(hostname);
if(ip != null){
List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
return inetAddresses;
}
return Dns.SYSTEM.lookup(hostname);
}
}

协议版本升级

1
2
3
- 1.0:版本TCP连接不复用
- 1.1:引入持久连接,但数据通讯按次序进行
- 2:多工,客户端、服务器双向实时通信

网络请求质量监控

1
2
接口请求耗时、成功率、错误码
图片加载的每一步耗时

网络容灾机制

1
2
备用服务器分流
多次失败后一定时间内不进行请求,避免雪崩效应

其它

1
2
CDN加速、提高带宽、动静资源分离(更新后清理缓存)
减少传输量,注意请求时机及频率

总结

1
2
网络请求质量指标
提升网络请求质量的方式

六、网络体系化方案建设

线下测试

1
2
方案:只抓单独App
侧重点:请求有误、多余,网络切换、弱网、无网测试

线上监控

1
2
3
4
5
6
7
8
9
10
11
12
服务端监控
- 请求耗时(区分地域、时间段、版本、机型
- 失败率(业务失败与请求失败)
- Top失败接口、异常接口
客户端监控
- 接口的每一步详细信息(DNS、连接、请求等)
- 请求次数、网络包大小、失败原因
- 图片监控
异常监控体系
- 服务器防刷:超限拒绝访问
- 客户端:大文件预警、异常兜底策略
- 单点问题追查

七、问题

1
2
3
4
5
6
7
8
9
10
11
12
在网络方面你们做了哪些监控,建立了哪些指标
- 演进过程、优化背景
- 质量:请求成功率、每步耗时、状态码
- 流量:精确统计、前后台
如何有效的降低用户流量消耗
- 数据:缓存、增量更新
- 上传:压缩
- 图片:缩略图、webp
用户反馈消耗流量多这种问题怎么查
- 精准流量获取能力
- 所有请求大小及次数的监控
- 主动预警能力
6.线程优化

一、线程调度原理分析

线程调度原理

1
2
3
1.任意时刻,只有一个线程占用CPU,处于运行状态
2.多线程并发:轮流获取CPU使用权
3.JVM负责线程调度:按照特定机制分配CPU使用权

线程调度模型

1
2
1.分时调度模型:轮流获取、均分CPU时间
2.抢占式调度模型:优先级高的获取,JVM采用

Android线程调度

1
2
3
4
5
6
7
1.nice值
- Process中定义
- 值越小,优先级越高
- 默认是THREAD_PRIORITY_DEFAULT,0
2.cgroup
- 更严格的群组调度策略(前台/后台)
- 保证前台线程可以获取到更多的CPU

注意点

1
2
3
1.线程过多会导致CPU频繁切换,降低线程运行效率
2.正确认识任务重要性决定哪种优先级(工作量越低,优先级应该越低)
3.优先级具有继承性(A线程中启动线程B,则B基础A的优先级)

二、android异步方式汇总

Thread

1
2
3
最简单、常见的异步方式
- 不易复用,频繁创建及销毁开销大
- 复杂场景不易使用

HandlerThread

1
2
3
自带消息循环的线程
- 串行执行
- 长时间运行,不断从队列中获取任务

IntentService

1
2
3
继承自Service在内部创建HandlerThread
- 异步,不占用主线程
- 优先级较高,不易被系统Kill

AsyncTask

1
2
3
Android提供的工具类
- 无需自己处理线程切换
- 需注意版本不一致问题(14以下存在,目前几乎不用管)

线程池

1
2
3
Java提供的线程池
- 易复用,减少频繁创建、销毁的时间
- 功能强大:定时、任务队列、并发数控制等

RxJava

1
2
由强大的 Scheduler 集合提供
- 不同类型的区分 :IO、Computation

异步方式总结

1
2
推荐度:从前往后排列
正确场景选择正确的方式

三、线程优化实战

线程使用准则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.严禁直接 new Thread
2.提供基础线程池供各个业务线使用
- 避免各个业务线各自维护一套线程池,导致线程数过多
3.根据任务类型选择合适的异步方式
- 优先级低,长时间执行,HandlerThread
4.创建线程必须命名
- 方便定位线程归属
- 运行期Thread.currentThread0.setName修改名字
5.关键异步任务监控
- 异步不等于不耗时
- 可通过AOP的方式来做监控
6.重视优先级设置
- Process.setThreadPriority();
- 可以设置多次;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//ThreadPoolUtils工具类
package com.optimize.performance.async;


import android.os.Process;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolUtils {

private int CPUCOUNT = Runtime.getRuntime().availableProcessors();

private ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(CPUCOUNT, CPUCOUNT,
30, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);

private ThreadPoolExecutor iOExecutor = new ThreadPoolExecutor(64, 64,
30, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);

public Thread newThread(Runnable r) {
return new Thread(r, "ThreadPoolUtils #" + mCount.getAndIncrement());
}
};

public static ExecutorService getService() {
return sService;
}

private static ExecutorService sService = Executors.newFixedThreadPool(5, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "ThreadPoolUtils");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
return thread;
}
});
}
1
2
3
4
5
6
7
8
9
10
11
//使用示例
ThreadPoolUtils.getService().execute(new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName("new Name");
LogUtils.i("");
Thread.currentThread().setName(oldName);
}
});

四、如何锁定线程创建者

锁定线程创建背景

1
2
3
1.项目变大之后收敛线程
2.项目源码、三方库、aar中都有线程的创建
3.避免恶化的一种监控预防手段

锁定线程创建方案

1
2
3
4
5
6
1.分析
- 创建线程的位置获取堆栈
- 所有的异步方式,都会走到new Thread
2.特别适合Hook手段
3.找Hook点:构造函数或者特定方法
4.Thread的构造函数
1
2
3
4
5
6
7
8
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
LogUtils.i(thread.getName()+" stack "+Log.getStackTraceString(new Throwable()));
}
});

五、线程收敛优雅实现

线程收敛常规方案

1
2
1.根据线程创建堆栈考量合理性,使用统一线程库
2.各业务线下掉自己的线程库

基础库怎么使用线程

1
2
1.直接依赖线程库
2.缺点:线程库更新可能会导致基础库更新

基础库优雅使用线程

1
2
1.基础库内部暴露API:setExecutor
2.初始化的时候注入统一的线程库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.optimize.performance.utils;

import android.util.Log;
import com.optimize.performance.PerformanceApp;
import java.util.concurrent.ExecutorService;

public class LogUtils {
private static ExecutorService sExecutorService;
public static void setExecutor(ExecutorService executorService){
sExecutorService = executorService;
}
public static final String TAG = "performance";
public static void i(String msg){
if(Utils.isMainProcess(PerformanceApp.getApplication())){
Log.i(TAG,msg);
}
// 异步
if(sExecutorService != null){
// sExecutorService.execute();
}
}
}

统一线程库

1
2
3
1.区分任务类型:IO、CPU密集型
2.IO密集型任务不消耗CPU,核心池可以很大
3.CPU密集型任务:核心池大小和CPU核心数相关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.optimize.performance.async;
import android.os.Process;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolUtils {

private int CPUCOUNT = Runtime.getRuntime().availableProcessors();

private ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(CPUCOUNT, CPUCOUNT,
30, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);

private ThreadPoolExecutor iOExecutor = new ThreadPoolExecutor(64, 64,
30, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);

public Thread newThread(Runnable r) {
return new Thread(r, "ThreadPoolUtils #" + mCount.getAndIncrement());
}
};

public static ExecutorService getService() {
return sService;
}

private static ExecutorService sService = Executors.newFixedThreadPool(5, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "ThreadPoolUtils");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
return thread;
}
});
}

六、问题

线程使用为什么会遇到问题

1
2
1.项目发展阶段
2.问题原因及表现形式

怎么在项目中对线程进行优化

1
2
3
1.线程收敛
2.统一线程池:任务区分
3.其他细节
5.卡顿优化
1
2
3
4
//卡顿优化
1.硬件性能导致卡顿:CPU Profiler/Systrace 分析查看CPU利用率
2.使用blockcanary可以实现自动化监测卡顿
3.anr检测:ANR-WatchDog,logcat/trace.txt日志分析信息

一、卡顿介绍及工具选择

背景介绍

1
2
1.很多性能问题不易被发现,但是卡顿很容易被直观感受
2.卡顿问题难以定位

CPU Profiler

1
2
3
4
5
6
7
1.图形的形式展示执行时间、调用栈等
2.信息全面,包含所有线程
3.运行时开销严重,整体都会变慢
4.使用方式
- Debug.startMethodTracing(");
- Debug.stopMethodTracing();
- 生成文件在sd卡:Android/data/packagename/files

Systrace

1
2
3
4
5
6
7
8
9
1.监控和跟踪Api调用、线程运行情况,生成Html报告
2.API18以上使用,推荐TraceCompat
3.使用方式
python systrace.py -t 10 [other-options] [categories]
文档:https://developer.android.com/studio/command-line/systrace#command options
4.优点
1.轻量级,开销小
2.直观反映CPU利用率
3.给出建议

StrictMode

1
2
3
4
5
6
7
8
9
10
11
1.严苛模式,Android提供的一种运行时检测机制
2.方便强大,容易被忽视
3.包含: 线程策略和虚拟机策略检测
4.线程策略
自定义的耗时调用,detectCustomSlowCalls()
磁盘读取操作,detectDiskReads
网络操作,detectNetwork
5.虚拟机策略
Activity泄露,detectActivityLeaks()
Sqlite对象泄露,detectLeakedSqlLiteObjects
检测实例数量,setClassInstanceLimit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//使用示例:
private void initStrictMode() {
if (DEV_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()// or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(NewsItem.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}

运行后的开始自动检测结果如下:image-20230919151328240

二、自动化卡顿检测方案及优化

为什么需要自动化检测方案

1
2
1.系统工具适合线下针对性分析
2.线上及测试环节需要自动化检测方案

方案原理

1
2
3
1.消息处理机制,一个线程只有一个Looper
2.mLogging对象在每个message处理前后被调用
3.主线程发生卡顿,是在dispatchMessage执行耗时操作

具体实现

1
2
3
1.Looper.getMainLooper().setMessageLogging();
2.匹配>>>>>Dispatching,闽值时间后执行任务(获取堆栈)
3.匹配<<<<<Finished,任务启动之前取消掉

AndroidPerformanceMonitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
非侵入式的性能监控组件,通知形式弹出卡顿信息
- com.github.markzhai:blockcanary-android
- https://github.com/markzhai/AndroidPerformanceMonitor
使用:
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
//application中执行初始化
BlockCanary.install(this, new AppBlockCanaryContext()).start();
//mainActivity演示Msg导致的主线程卡顿
new Handler().post(new Runnable() {
@Override
public void run() {
LogUtils.i("Msg 执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//自定义类
package com.optimize.performance.block;

import android.content.Context;
import android.util.Log;

import com.github.moduth.blockcanary.BlockCanaryContext;
import com.github.moduth.blockcanary.internal.BlockInfo;

import java.io.File;
import java.util.LinkedList;
import java.util.List;

/**
* BlockCanary配置的各种信息
*/
public class AppBlockCanaryContext extends BlockCanaryContext {

/**
* Implement in your project.
*
* @return Qualifier which can specify this installation, like version + flavor.
*/
public String provideQualifier() {
return "unknown";
}

/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}

/**
* Network type
*
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
public String provideNetworkType() {
return "unknown";
}

/**
* Config monitor duration, after this time BlockCanary will stop, use
* with {@code BlockCanary}'s isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
public int provideMonitorDuration() {
return -1;
}

/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 500;
}

/**
* Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
* stack according to current sample cycle.
* <p>
* Because the implementation mechanism of Looper, real dump interval would be longer than
* the period specified here (especially when cpu is busier).
* </p>
*
* @return dump interval (in millis)
*/
public int provideDumpInterval() {
return provideBlockThreshold();
}

/**
* Path to save log, like "/blockcanary/", will save to sdcard if can.
*
* @return path of log files
*/
public String providePath() {
return "/blockcanary/";
}

/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}

/**
* Implement in your project, bundle files into a zip file.
*
* @param src files before compress
* @param dest files compressed
* @return true if compression is successful
*/
public boolean zip(File[] src, File dest) {
return false;
}

/**
* Implement in your project, bundled log files.
*
* @param zippedFile zipped file
*/
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}


/**
* Packages that developer concern, by default it uses process name,
* put high priority one in pre-order.
*
* @return null if simply concern only package with process name.
*/
public List<String> concernPackages() {
return null;
}

/**
* Filter stack without any in concern package, used with @{code concernPackages}.
*
* @return true if filter, false it not.
*/
public boolean filterNonConcernStack() {
return false;
}

/**
* Provide white list, entry in white list will not be shown in ui list.
*
* @return return null if you don't need white-list filter.
*/
public List<String> provideWhiteList() {
LinkedList<String> whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}

/**
* Whether to delete files whose stack is in white list, used with white-list.
*
* @return true if delete, false it not.
*/
public boolean deleteFilesInWhiteList() {
return true;
}

/**
* Block interceptor, developer may provide their own actions.
*/
public void onBlock(Context context, BlockInfo blockInfo) {
Log.i("lz","blockInfo "+blockInfo.toString());
}
}

代码运行后点击顶部通知栏出现下图:image-20230920092229592

方案总结

1
2
3
4
5
1.非侵入式
2.方便精准,定位到代码某一行
缺点:
1.确实卡顿了,但卡顿堆栈可能不准确
2.和OOM一样,最后的堆栈只是表象,不是真正问题

image-20230920092401457

自动检测方案优化

1
2
获取监控周期内的多个堆栈,而不仅是最后一个
startMonitor->高频采集堆栈->endMonitor->记录多个堆栈->上报

海量卡顿堆栈处理

1
2
3
4
高频卡顿上报量太大,服务端有压力
分析:一个卡顿下多个堆栈大概率有重复
解决:对一个卡顿下堆栈进行hash排重,找出重复的堆栈
效果:极大的减少展示量同时更高效找到卡顿堆栈

三、ANR分析与实战

ANR介绍

1
2
3
KeyDispatchTimeout,5s
BroadcastTimeout,前台10s,后台60s
ServiceTimeout,前台20s,后台200s

ANR执行流程

1
2
3
发生ANR
进程接收异常终止信号,开始写入进程ANR信息
弹出ANR提示框(Rom表现不一,有的手机可能不弹)

ANR解决套路

1
2
adb pull data/anr/traces.txt 存储路径(发送anr系统会保存自动保存信息到该文件)
详细分析:CPU、IO、锁

模拟anr触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 @Override
protected void onCreate(Bundle savedInstanceState) {
...
new Thread(){
@Override
public void run() {
super.run();
synchronized (MainActivity.this){
try {
Thread.sleep( millis: 20000);
} catch (InterruptedException e) {
e.printStackTrace();
}.start();
...
synchronized (MainActivity.this){
LogUtils.i( msg:");
}
...
}

发送anr(弹框弹出来的时间不止5m钟,因为系统会先将anr信息写入文件后再弹窗)image-20230920093842853

执行adb pull data/anr/traces.txt后分析日志

线上ANR监控方案

1
通过FileOberver监控文件变化,高版本权限问题(高版本上权限问题导致无法监控到文件变化)

ANR-WatchDog

1
2
3
4
5
6
7
8
9
10
11
非侵入式的ANR监控组件
- com.github.anrwatchdog:anrwatchdog:1.3.0
- https://github.com/SalomonBrys/ANRWatchDog
使用:
new ANRWatchDog().start();(Application中)
ANR-WatchDog原理:
start->post消息改值->sleep->检测是否修改->判断ANR发生->抛出异常(线下调试方便,自动定位到异常代码位置)
//可以复写他的异常处理代码,改为上传到自己服务器上而不跑出异常
优点:
- 非侵入式
- 弥补高版本无权限问题

区别

1
2
3
1.AndroidPerformanceMonitor:监控Msg
2.ANR-WatchDog :看最终结果
3.前者适合监控卡顿,后者适合补充ANR监控

四、卡顿单点问题检测方案

背景介绍

1
2
3
1.自动卡顿监测方案并不够(有卡顿但是不到阈值)
2.体系化解决方案务必尽早暴露问题
3.单点问题:主线程IPC、DB

IPC问题监测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
监测指标
- IPC调用类型
- 调用耗时、次数
- 调用堆栈、发生线程
常规方案
- IPC前后加埋点
- 不优雅、容易忘记
- 维护成本大
IPC问题监测技巧
- adb 命令
- adb shell am trace-ipc start
- adb shell am trace-ipc stop -dump-file/data/local/tmp/ipc-trace.txt
- adb pull /data/local/tmp/ipc-trace.txt
IPC问题监测
- 优雅方案
- ARTHook 还是AspectJ ?
- ARTHook:可以Hook系统方法
- AspectJ:非系统方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
示例:
try {
DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
+ "\n" + Log.getStackTraceString(new Throwable()));
super.beforeHookedMethod(param);
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

卡顿问题监测方案

1
2
1.利用ARTHook完善线下工具
2.开发阶段Hook相关操作,暴露、分析问题

监控纬度

1
2
3
1.IPC
2.IO、DB
3.View绘制

五、如何实现界面秒开

界面秒开实现

1
2
3
1.SysTrace,优雅异步+优雅延迟初始化
2.异步Inflate、X2C、绘制优化
3.提前获取页面数据

界面秒开率统计

1
2
1.onCreate到onWindowFocusChanged
2.特定接口

Lancet

1
2
3
4
5
6
7
轻量级Android AOP框架
- 编译速度快,支持增量编译
- API简单,没有任何多余代码插入 apk
- https://github.com/eleme/lancet
使用介绍
- @Proxyi通常用与对系统API调用的Hook
- @Insert 常用于操作App与library的类

使用示例:

1
2
3
implementation 'me.ele:lancet-base:1.0.4'
//相当于hook acitivity的生命周期实现展开时间的计算
//也可自定义hook其他系统方法,如log.i实现自定义消息拼接或处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.optimize.performance.aop;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;

import com.optimize.performance.utils.LogUtils;
import com.optimize.performance.wakelock.WakeLockUtils;

import me.ele.lancet.base.Origin;
import me.ele.lancet.base.Scope;
import me.ele.lancet.base.annotations.Insert;
import me.ele.lancet.base.annotations.Proxy;
import me.ele.lancet.base.annotations.TargetClass;

public class ActivityHooker {

public static ActivityRecord sActivityRecord;

static {
sActivityRecord = new ActivityRecord();
}

public static String trace;

@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
sActivityRecord.mOnCreateTime = System.currentTimeMillis();
Origin.callVoid();
}

@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
LogUtils.i("onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}


public static long sStartTime = 0;

@Insert(value = "acquire")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void acquire(Context context){
trace = Log.getStackTraceString(new Throwable());
sStartTime = System.currentTimeMillis();
Origin.callVoid();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
WakeLockUtils.release();
}
},1000);
}

@Insert(value = "release")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void release(){
LogUtils.i("PowerManager "+(System.currentTimeMillis() - sStartTime)+"/n"+trace);
Origin.callVoid();
}


public static long runTime = 0;

@Insert(value = "run")
@TargetClass(value = "java.lang.Runnable",scope = Scope.ALL)
public void run(){
runTime = System.currentTimeMillis();
Origin.callVoid();
LogUtils.i("runTime "+(System.currentTimeMillis() - runTime));
}

@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag, String msg) {
msg = msg + "";
return (int) Origin.call();
}
}


package com.optimize.performance.aop;
public class ActivityRecord {
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}

界面秒开监控纬度

1
2
3
1.总体耗时
2.生命周期耗时
3.生命周期间隔耗时

六、优雅监控耗时盲区

耗时盲区监控背景

1
2
3
1.生命周期的间隔
2.onResume到Feed展示的间隔
3.举例 : postMessage,很可能在Feed之前执行(handler的消息中存在耗时操作)

耗时盲区监控难点

1
2
1.只知道盲区时间,不清楚具体在做什么
2.线上盲区无从追查

耗时盲区监控线下方案

1
2
3
TraceView
- 特别适合一段时间内的盲区监控
- 线程具体时间做了什么,一目了然

耗时盲区监控线上方案

1
2
3
4
思考分析
- 所有方法都是Msg,mLogging ?没有Msg具体堆栈
- AOP切Handler方法?不清楚准确执行时间
//上面2种方式都有各自的问题

耗时盲区监控线上方案

1
2
1.使用统一的Handler:定制具体方法
2.定制gradle插件,编译期动态替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.optimize.performance.handler;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.optimize.performance.utils.LogUtils;

import org.json.JSONObject;

public class SuperHandler extends Handler {

private long mStartTime = System.currentTimeMillis();

public SuperHandler() {
super(Looper.myLooper(), null);
}

public SuperHandler(Callback callback) {
super(Looper.myLooper(), callback);
}

public SuperHandler(Looper looper, Callback callback) {
super(looper, callback);
}

public SuperHandler(Looper looper) {
super(looper);
}

@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean send = super.sendMessageAtTime(msg, uptimeMillis);
if (send) {
GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
}
return send;
}

@Override
public void dispatchMessage(Message msg) {
mStartTime = System.currentTimeMillis();
super.dispatchMessage(msg);

if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
&& Looper.myLooper() == Looper.getMainLooper()) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));

LogUtils.i("MsgDetail " + jsonObject.toString());
GetDetailHandlerHelper.getMsgDetail().remove(msg);
} catch (Exception e) {
}
}
}

}

//视频中使用的普通handler发送消息后拿到的log日志

耗时盲区监控方案总结

1
2
3
1.卡顿监控重要的一环,全面性保障
2.TraceView适合线下,可以监控系统Msg
3.动态替换适合线上,只有应用自身的Msg

七、卡顿优化技巧总结

卡顿优化实践经验

1
2
3
耗时操作:异步、延迟
布局优化:异步Inflate、X2C、重绘解决
内存:降低内存占用,减少GC时间

卡顿优化工具建设

1
2
3
4
5
6
7
8
9
10
11
12
13
14
系统工具认识、使用
- SysTrace
- TraceView
- StrictMode
自动化监控及优化
- AndroidPerformanceMonitor、ANR-WatchDog
- 高频采集,找出重复率高的堆栈
卡顿监控工具
- 单点问题:AOP、Hook
- 盲区监控: gradle编译期替换
卡顿监控指标
- 卡顿率、ANR率、界面秒开率
- 交互时间、生命周期时间
- 上报环境、场景信息

八、问题

1
2
3
4
5
6
7
8
9
10
11
1.你是怎么做卡顿优化的
- 第一阶段:系统工具定位、解决
- 第二阶段:自动化卡顿方案及优化
- 第三阶段:线上监控及线下监测工具建设
2.怎么自动化的获取卡顿信息
- mLogging.println
- 高频采集,找出重复堆栈
3.卡顿的一整套解决方案是怎么做的
- 线下、线上工具相结合
- 特定难题突破:单点问题、盲区监控
- 线上监控建设
4.布局优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//布局优化
1. 绘制时长检测:
1.AOP(面向切面编程,在指定的地方执行特定的代码)的方式获取每个界面的绘制耗时;
1.Systrace查看整体绘制情况,查看是否存在绘制耗时等情况;
2.as自带的布局检测工具Inspector,查看视图层次结构;
2.如存在io加载布局文件慢
1.使用AsyncLayoutInflater/X2C异步加载布局
3. 布局层级:减少布局层级,减少过度绘制
1.减少View树层级
2.宽而浅,避免窄而深
3.尽量使用 ConstraintLayout布局控件
2.gpu过度绘制//开发者选项-调试gpu过度绘制打开设置
1.一个像素最好只被绘制一次
2.调试GPU过度绘制
3.蓝色可接受

3. 卡顿的原因就是CPU数据处理不过来,比如层级过深,CPU太忙,GC,之类的,可以通过systrace 工具进行检测,
然后通过编舞者监测帧率定位时间,再然后可以通过looper的日志功能定位到卡顿点。网上也有库blockcanary

3.在Systrace 报告中,你可以查看应用程序布局绘制的相关事件,包括布局计算、绘制命令的发送、GPU 图形呈现等阶段所花费的时间

一、绘制原理及工具选择

绘制原理

1
2
3
4
1.CPU负责计算显示内容
2.GPU负责栅格化(UI元素绘制到屏幕上)
3.16ms发出VSync信号触发UI渲染
4.大多数的Android设备屏幕刷新频率:60Hz

优化工具

1
2
3
4
Systrace
- 关注Frames
- 正常:绿色圆点,丢 :黄色或红色
- Alerts栏

image-20230919111203321

Layout Inspector

1
2
3
1.AndroidStudio自带工具
2.查看视图层次结构
androidStudio顶部tools-Layout Inspector-选择对应进程-生成当前界面的检测信息

image-20230919111522587

Choreographer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
获取FPS,线上使用,具备实时性
- Api 16之后
- Choreographer.getInstance().postFrameCallback
使用示例:

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}

获取fps的日志打印image-20230919111909078

二、布局加载原理

image-20230919113039306

性能瓶颈

1
2
布局文件解析:IO过程
创建View对象:反射

LayoutInflater.Factory

1
2
LayoutInflater创建View的一个Hook
定制创建View的过程:全局替换自定义TextView等

Factory与Factory2

1
2
Factory2继承于Factory
多了一个参数:parent

三、优雅获取界面布局耗时

常规方式

1
2
背景:获取每个界面加载耗时
实现:覆写方法、手动埋点

AOP实现

1
2
切Activity的setContentView
@Around("execution(*android.app.Activity.setContentView(..))")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.optimize.performance.aop;
import com.optimize.performance.utils.LogUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class PerformanceAop {
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
}
}

随意点击2个页面后查看日志:image-20230919114258052

ARTHook实现:切Activity的setContentView

获取每一个控件加载耗时

1
2
1.低侵入性
2.LayoutInflater.Factory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.equals(name, "TextView")) {
// 生成自定义TextView
}
long time = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
...//需要在下面2句代码之前执行否则无效
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
...
}

四、异步Inflate实战

背景介绍

1
2
1.布局文件读取慢:IO过程
2.创建View慢:反射(比new3倍)

AsyncLayoutInflater

1
2
3
4
简称异步Inflate
- WorkThread加载布局
- 回调主线程
- 节约主线程时间

AsyncLayoutInflater使用(相当于侧面解决方案,从子线程加载xml资源文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com.android.support:asynclayoutinflater

@Override
protected void onCreate(Bundle savedInstanceState) {
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view);
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
mRecyclerView.setAdapter(mNewsAdapter);
mNewsAdapter.setOnFeedShowCallBack(MainActivity.this);
}
});
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
...
}

AsyncLayoutInflater缺点

1
2
1.不能设置LayoutInflater.Factory (自定义解决
2.注意View中不能有依主线程的操作

Java代码写布局

1
2
1.本质上解决了性能问题
2.引入新问题:不便于开发、可维护性差

X2C介绍

1
2
3
保留XML优点,解决其性能问题
- 开发人员写XML,加载Java代码
- 原理:APT编译期翻译XML为Java代码(相当于自动将xml布局转为java代码后再加载)

X2C使用

1
2
3
4
5
6
7
8
9
10
11
12
13
1.AnnotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'(appMoudle的build.gradle中添加)
2.implementation 'com.zhangyue.we:x2c-lib:1.0.6'
3.@Xml(layouts = "activity_main")
(类名上面,如:
@Xml(layouts = "activity_main")
public class MainActivity extends AppCompatActivity){
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
X2C.setContentView(MainActivity.this, R.layout.activity_main);
}
)
//build中可以看到出现了2个MainActivity,一个是x2c将xml转为java代码的类

X2C问题

1
2
1.部分属性Java不支持
2.失去了系统的兼容(AppCompat)

五、视图绘制优化

视图绘制流程

1
2
3
测量:确定大小(自顶向下遍历确定每个布局的大小)
布局:确定位置(根据测量大小确定控件位置)
绘制:绘制视图

性能瓶颈

1
2
3
1.每个阶段耗时
2.自顶而下的遍历
3.触发多次

布局层级及复杂度

1
2
3
准则
1.减少View树层级
2.宽而浅,避免窄而深

ConstraintLayout

1
2
3
1.实现几乎完全扁平化布局
2.构建复杂布局性能更高
3.具有RelativeLayout和LinearLayout特性

布局层级及复杂度

1
2
3
1.不嵌套使用RelativeLayout
2.不在嵌套LinearLayout中使用weight
3.merge标签:减少一个层级,只能用于根View

过度绘制

1
2
3
4
//过度绘制-开发者选项-调试gpu过度绘制打开设置
1.一个像素最好只被绘制一次
2.调试GPU过度绘制
3.蓝色可接受

避免过度绘制方法

1
2
3
1.去掉多余背景色,减少复杂shape使用
2.避免层级叠加
3.自定义View使用clipRect屏蔽被遮盖View绘制

其它技巧

1
2
3
1.Viewstub:高效占位符、延迟初始化
2.onDraw中避免:创建大对象、耗时操作
3.TextView优化

六、问题

1
2
3
4
5
6
7
8
9
10
11
你在做布局优化过程中用到了哪些工具
- Choreographer
- AOP、Hook
- Systrace、Layout Inspector
布局为什么会导致卡顿,你又是怎么优化的 ?
- IO、反射、遍历、重绘
- 异步Inflate、X2C、减少层级、重绘
做完布局优化有哪些成果产出
- 体系化监控手段:线下+线上
- 指标:FPS、加载时间、布局层级
- 核心路径保障
3.内存优化
1
2
3
4
//内存优化:内存优化主要体现在两个地方,第一个是内存抖动,第二个是内存泄漏
1. 内存抖动: 主要是由于快速的内存创建和释放造成的,容易产生GC(Profiler);
2. 内存泄漏:主要是由于长生命周期的引用了短生命周期的对象造成的(leakcary);
3. 检测方式:AS的Profiler(Memory Profiler/Memory Analyzer)主动检测,leakcary被动的自动检测;

一、背景介绍

1
2
1.内存是大问题但缺乏关注
2.压死骆驼的最后一根稻草

内存问题

1
2
3
1.内存抖动:锯齿状、GC导致卡顿
2.内存泄露:可用内存减少、频繁GC
3.内存溢出:OOM、程序异常

工具选择

1
2
3
1.Memory Profiler
2.Memory Analyzer
3.LeakCanary

Memory Profiler(点击Studio底部Android Profiler界面中MEMORY区域的任意位置,即可开启Memory Profiler)

1
2
3
4
5
6
优点:
1.实时图表展示应用内存使用量
2.识别内存泄露、抖动等
3.提供捕获堆转储、强制GC以及跟踪内存分配的能力
4.方便直观
5.线下平时使用

img

1
2
3
4
5
6
7
8
9
1对应的按键用于强制内存回收。
2对应的按键用于抓取进程内存的dump信息。
3对应的按键用于记录内存的分配信息(连接Android 7.1及以下才会有此按键)。
初次点击时,对应统计的开始时间点;再次点击时,对应统计的结束时间点。
进程在两个时间点之间的内存分配信息,将被Memory Profiler记录和分析。
4对应的区域用于缩放时间轴。
5对应的按键用于显示实时的内存数据。
6对应的区域用于记录事件发生的时间点及大致持续的时间(例如activity状态改变、用户操作界面等事件)。
7对应的区域用于显示内存使用情况对应的时间轴(与标注6结合,就可以看出各事件带来的内存变化情况)。

img

1
2
3
4
5
6
7
8
9
如上图所示,其中: 
Java表示Java代码或Kotlin代码分配的内存;
Native表示C或C++代码分配的内存(即使App没有native层,调用framework代码时,也有可能触发分配native内存);
Graphics表示图像相关缓存队列占用的内存;
Stack表示native和java占用的栈内存;
Code表示代码、资源文件、库文件等占用的内存;
Others表示无法明确分类的内存;
Allocated表示Java或Kotlin分配对象的数量(Android8.0以下时,仅统计Memory Profiler启动后,进程再分配的对象数量;
8.0以上时,由于系统内置了统计工具,Memory Profiler可以得到整个app启动后分配对象的数量)。

点击左上角的标记2按钮,则出现下图可以查看各个对象的对应信息,如bitmap的大小和预览图image-20230918154331037

Memory Analyzer

1
2
3
强大的Java Heap分析工具,查找内存泄露及内存占用
生成整体报告、分析问题等
线下深入使用

LeakCanary

1
2
3
1.自动内存泄露检测
文档:https://github.com/square/leakcanary
2.线下集成

二、Android内存管理机制

Java内存区域划分

1
2
3
4
5
6
7
8
1.方法区(Method Area):用于存储类的信息、常量、静态变量等,方法区在程序启动时就被创建,所有线程共享。
其中,常量池用于存放字符串常量、类和接口常量等.
2.堆(Heap):用于存储对象实例。堆在程序运行时动态分配和回收,由垃圾回收器负责管理。
堆可以分为新生代和老年代两部分新生代又分为Eden空间、Survivor 0空间和Survivor 1空间.
虚拟机栈(VM Stack):每个线程在运行时都会创建一个栈,用于存储方法的局部变量、操作数栈、动态连接等。
栈顺随着方法3的进入和退出而动态创建和销毁。
4.本地方法栈(Native Method Stack): 与虚拟机栈类似,但用于执行本地方法(即使用其他语言编写的方法)
5.程序计数器(Program Counter) : 用于记录当前线程执行的字节码指令地址。

java内存回收算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 标记-清除算法(Mark and Sweep):
首先标记所有活动对象,然后清除所有未标记的对象。这种算法存在的问题是标记和清除效率不高并会产生大量的碎片,
导致内存空间的浪费。

2. 复制算法(Copying):
2.1将内存划分为大小相等的两块
2.2一块内存用完之后复制存活对象到另一块
2.3清理另一块内存
2.4实现简单,运行高效(相对标记算法而言)
2.5浪费一半空间,代价大
将内存分为两个区域,每次只使用其中一个区域,当该区域满了之后,将所有活动对象复制到另一个区域中,
然后清除原来的区域。这种算法的优点是简单高效,但是会浪费一半的内存空间。

3. 标记-整理算法(Mark and Compact):
1.标记过程与“标记-清除”算法一样
2.存活对象往一端进行移动
3.清理其余内存
4.避免标记-清理导致的内存碎片
5.避免复制算法的空间浪费
首先标记所有活动对象,然后将它们移动到内存的一端,然后清除另一端的所有未标记对象。
这种算法可以避免碎片问题,但是需要移动对象,可能会影响程序的性能。

4. 分代收集算法(Generational):
1.结合多种收集算法优势
2.新生代对象存活率低,复制
3.老年代对象存活率高,标记-整理
将内存分为几个代,每个代的对象生命周期不同。新创建的对象放在第一代,经过多次回收后仍然存活的对象会被移到下一代,
最终存活的对象会被移到老年代。这种算法可以根据对象的生命周期来优化回收效率。

5. 引用计数算法(Reference Counting):
对每个对象维护一个引用计数器,当有新的引用指向该对象时,计数器加一,当引用失效时,计数器减一。
当计数器为零时,说明该对象已经不再被引用,可以回收。这种算法的问题是无法处理循环引用的情况

Android内存管理机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.内存弹性分配,分配值与最大值受具体设备影响
2.OOM场景:内存真正不足、可用内存不足
3.Dalvik与Art区别
1.Dalvik仅固定一种回收算法
2.Art回收算法可运行期选择(5.0之后都是这种算法)
3.Art具备内存整理能力,减少内存空洞
4.Low Memory Killer
1.进程分类(根据进程下面优先级,先考虑优先级低的进程进行回收)
1.前台进程
2.可见进程
3.服务进程
4.后台进程
5.空进程
2.回收收益

三、内存抖动实战

内存抖动介绍

1
2
3
定义:内存频繁分配和回收导致内存不稳定
表现:频繁GC、内存曲线呈锯齿状
危害:导致卡顿、OOM

内存抖动导致OOM

1
2
1.频繁创建对象,导致内存不足及碎片(不连续)
2.不连续的内存片无法被分配,导致OOM

内存抖动解决实战:使用Memory Profiler初步排查

创建一个模拟内存抖动的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.optimize.performance.memory;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

import com.optimize.performance.R;

/**
* 模拟内存抖动的界面
*/
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {

@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 创造内存抖动
for (int index = 0; index <= 100; index++){
String arg[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0,30);
}
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory);
findViewById(R.id.bt_memory).setOnClickListener(this);

}

@Override
public void onClick(View v) {
mHandler.sendEmptyMessage(0);
}

@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}

点击执行后发送内存抖动时Memory Profiler的现象:image-20230918171625886

查找具体引起抖动的方式:点击record开始记录,再点击stop停止记录(如下图);

image-20230918172026002

左侧可以查看到不同对象的size大小,选中后右侧上面随意选择一个对象底部则会显示对象的调用信息,点击右键出现Jump to Source跳转到方法的调用处;

内存抖动解决技巧:找循环或者频繁调用的地方

四、内存泄漏实战

内存泄露介绍

1
2
3
定义:内存中存在已经没有用的对象
表现:内存抖动、可用内存逐渐变少
危害:内存不足、GC频繁、OOM

Memory Analyzer

1
2
文档:https://www.eclipse.org/mat/downloads.ph
转换: hprof-conv 原文件路径 转后文件路径

模拟内存泄漏代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.optimize.performance.memory;
import java.util.ArrayList;
public class CallBackManager {
public static ArrayList<CallBack> sCallBacks = new ArrayList<>();
public static void addCallBack(CallBack callBack) {
sCallBacks.add(callBack);
}
public static void removeCallBack(CallBack callBack) {
sCallBacks.remove(callBack);
}
}


package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import com.optimize.performance.R;
/**
* 模拟内存泄露的Activity
*/
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
//这行解决内存泄漏的代码
CallBackManager.removeCallBack(this);
}
@Override
public void dpOperate() {
// do sth
}
}

通过多次进入再退出上面的MemoryLeakActivity类则会出现下图这种内存泄漏情况:image-20230919091415496

通过record左边的down按钮并保存文件,再通过命令转换文件格式:

image-20230919091622418

在使用Eclipse Memory Analyzer软件打开转换后的文件

image-20230919091803750

点击 Histogram 按钮

image-20230919091925652

在上图位置可以搜索怀疑存在内存泄漏的类,并查看显示的该类剩余的对象数量;

选中类的对象右键点击List Objects->with incoming references(哪些强引用引向了我)->选中类对象->Path to GC->with all references(所有对象都计算在内)分析后如下图,有个小圆点的则是目标内存泄漏的类

image-20230919092826271

总结

1
2
1.使用Memory Profiler初步观察
2.通过Memory Analyzer结合代码确认

五、MAT的使用

1
2
3
MAT工具全称为Memory Analyzer Tool,一款详细分析Java堆内存的工具,该工具非常强大

代码太多,可百度查看使用教程:如https://www.jianshu.com/p/034b72b91d3a/

六、优雅的检测不合理的图片

Bitmap内存模型

1
2
3
4
5
6
1.API10之前Bitmap自身在Dalvik Heap中,像素在Native
2.API10之后像素也被放在Dalvik Heap中
3.API26之后像素在Native
4.获取Bitmap占用内存
- getByteCount
- 宽*高*一像素占用内存

常规方式

1
2
3
当景:图片对内存优化至关重要、图片宽高大于控件宽高
实现:继承ImageView,覆写实现计算大小
缺点:侵入性强,不通用

ARTHook介绍

1
2
3
4
5
6
7
8
挂钩,将额外的代码钩住原有方法,修改执行逻辑
- 运行时插桩
- 性能分析
优点:
- 无侵入性
- 通用性强
缺点:
- 兼容问题大,开源方案不能带的线上环境

Epic简介

1
2
3
4
5
6
7
Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架
- 支持Android4.0-9.0
- https://github.com/tiann/epic
Epic使用
- compile 'me.weishu:epic:0.3.6
- 继承XC_MethodHook,实现相应逻辑
- 注入Hook : DexposedBridge.findAndHookMethod

实战自定义Epic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//compile 'me.weishu:epic:0.3.6

package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

import com.optimize.performance.utils.LogUtils;
import com.taobao.android.dexposed.XC_MethodHook;

public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 实现我们的逻辑
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}

private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
int width = view.getWidth();
int height = view.getHeight();
if (width > 0 && height > 0) {
// 图标宽高都大于view带下的2倍以上,则警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}


private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
.append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
.append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
.toString();

LogUtils.i(warnInfo);
}
}
1
2
3
4
5
6
7
8
//调用执行ImageHook
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});

再在MemoryLeakActivity的activity_memoryleak的xml中将imageview宽高修改为50

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv_memoryleak"
android:layout_width="50dp"
android:layout_height="50dp" />

</android.support.constraint.ConstraintLayout>

启动下面MemoryLeakActivity类,然后会执行检测,运行后弹出警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import com.optimize.performance.R;
/**
* 模拟内存泄露的Activity
*/
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
CallBackManager.removeCallBack(this);
}
@Override
public void dpOperate() {
// do sth
}
}

七、线上内存监控方案

常规实现一

1
设定场景线上Dump:Debug.dumpHprofData()

常规实现一流程

1
超过最大内存80%(只在特定情况下执行,如占用可用内存超过80%)->内存Dump->回传文件(wifi或无流量空闲时上传)->MAT手动分析

实现一问题:

1
2
3
1.Dump文件太大,和对象数正相关,可裁剪
2.上传失败率高、分析困难
3.配合一定策略,有一定效果

常规实现二

1
2
3
1.LeakCanary带到线上
2.预设泄露怀疑点
3.发现泄露回传

实现二问题:

1
2
1.不适合所有情况,必须预设怀疑点
2.分析比较耗时、也容易OOM

LeakCanary原理

1
2
3
4
1.监控生命周期,onDestroy添加RefWatcher检测
2.二次确认断定发生内存泄露
3.分析泄露,找引用链
4.监控组件+分析组件

LeakCanary定制

1
2
3
1.预设怀疑点->自动找怀疑点
2.分析泄露链路慢->分析Retain size大的对象
3.分析OOM->对象裁剪,不全部加载到内存

线上监控完整方案

1
2
3
1.待机内存、重点模块内存、OOM率
2.整体及重点模块GC次数、GC时间
3.增强的LeakCanary自动化内存泄露分析

八、内存优化技巧总结

优化大方向

1
2
3
1.内存泄露
2.内存抖动
3.Bitmap

优化细节

1
2
3
4
5
6
1.LargeHeap属性(如:系统默认256m,申请了LargeHeap属性则默认内存*2
2.onTrimMemory(低内存回调,低内存时主动做一些处理,关闭一些功能等)
3.使用优化过的集合:SparseArray
4.谨慎使用SharedPreference(调用时会将缓存的东西全部加载到内存中去,或者大数据量的东西不要用SharedPreference)
5.谨慎使用外部库(怕存在隐患)
6.业务架构设计合理

九、常见问题

1
2
3
4
5
6
7
8
9
10
11
1.你们内存优化项目的过程是怎么做的
- 分析现状、确认问题
- 针对性优化
- 效率提升
2.你做了内存优化最大的感受是什么
- 磨刀不误砍柴工
- 技术优化必须结合业务代码
- 系统化完善解决方案
3.如何检测所有不合理的地方
- ARTHook
- 重点强调区别
2.App启动优化
1
2
3
4
5
//App启动优化
1.获取启动耗时:
1.手动埋点,aop统一处理,systrace/traceview获取
2.优化手段:延迟加载或者异步加载(充分利用CPU,将任务放到子线程中执行分担主线程任务,jetpack的App Startup框架,或者自定义使
用线程池异步加载)

一、启动优化介绍

背景介绍

1
2
1.第一体验(应用启动的速度是第一感受)
2.八秒定律(如果一个页面8秒还没有响应,70%的用户将放弃等待)

启动分类

1
2
3
4
App startup time
- 冷启动
- 热启动
- 温启动

冷启动: 耗时最多,衡量标准 (经历了一系列的流程,因此耗时较多)

image-20230914162857993

热启动:最快 image-20230914163028372

温启动:较快 (重走activity的生命周期,不会重走进程的创建或application的创建等)image-20230914163202924

相关任务

1
2
3
4
5
6
7
8
9
10
11
12
1.冷启动之前(系统的操作)
- 启动App
- 加载空白Window
- 创建进程
2.随后任务
- 创建Application
- 启动主线程
- 创建MainActivity
3.随后任务
- 加载布局
- 布置屏幕
- 首帧绘制

优化方向

1
Application和Activity生命周期这个阶段

二、启动时间的测量方式

1
2
1.adb命令
2.手动打点

adb命令

1
2
3
4
5
6
7
adb shell am start -W packagename/首屏Activity
- ThisTime:最后一个Activity启动耗时
- TotalTime:所有Activity启动耗时
- WaitTime:AMS启动Activity的总耗时
缺点:
- 线下使用方便,不能带到线上
- 非严谨、精确时间
image-20230914163945871

手动打点

1
2
3
1.精确,可带到线上,推荐使用
2.避开误区,采用Feed第一条展示
3.addOnDrawListener要求API16
1
2
3
4
5
6
7
8
9
10
11
//方法类
public class LaunchTimer {
private static long sTime;
public static void startRecord() {
sTime = System.currentTimeMillis();
}
public static void endRecord(){
long cost = System.currentTimeMillis() - sTime;
Log.e( "tyl","cost=" + cost);
}
}
1
2
3
4
5
6
7
8
startTime的执行地方:
- Application的attachBaseContext方法中
endTime的执行地方:
- 误区: activity的onWindowFocusChanged只是首帧时间,用户还没有看到画面
- 正解:真实数据展示,Feed第一条展示 (recycleView的adapter的onBindViewHolder()->
holder.linearLayout.getViewTreeObserver().addOnPreDrawListener()->
onPreDraw()中执行
//onWindowFocusChanged和Feed中执行结束相差100多ms

三、启动优化工具选择

1
2
1.traceview
2.systrace

注意

1
2
- 两种方式互相补充
- 正确认识工具及不同场景选择合适的工具

traceview和systrace各自的作用和区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Traceview 和 Systrace 都是用于 Android 应用程序性能分析和调试的工具,但它们在功能和用途上有一些区别:

Traceview:
Traceview 是 Android 开发工具包(SDK)中提供的一种效率分析工具,用于分析应用程序的方法调用、线程执行时间等信息。
开发人员可以使用 Traceview 工具来查看应用程序的性能数据,包括方法的执行时间、CPU 使用率、线程的执行情况等。
Traceview 会生成一个包含方法调用图、执行时间和线程等信息的 trace 文件,开发人员可以通过 Android Studio 或其他工具进行分析和
查看。
主要用于分析应用程序代码的性能瓶颈、方法执行耗时等问题,帮助优化应用程序的性能。

Systrace:
Systrace 是 Android 系统级的跟踪工具,用于分析整个系统的性能数据,包括应用程序、系统服务、内核等的运行情况。
开发人员可以使用 Systrace 工具来查看应用程序与系统之间的交互情况、系统资源占用情况、视图绘制耗时等信息。
Systrace 通过采集系统事件、函数调用、线程、CPU 使用情况等信息,生成一个详细的时间线,展示系统运行时的各种活动。
主要用于分析应用程序与系统交互导致的性能问题,如绘制卡顿、I/O 操作慢、系统资源竞争等,帮助优化应用程序与系统的性能。

总的来说,Traceview 主要用于分析应用程序代码的性能问题,而 Systrace 则更侧重于分析整个系统的性能问题,包括应用程序与系统之
间的交互。开发人员可以根据具体情况选择合适的工具进行性能分析和调试,以优化应用程序的性能和稳定性。

traceview

1
2
3
traceview的优点:
图形的形式展示执行时间、调用栈等
信息全面,包含所有线程

使用方式(查看指定方法执行时间)

1
2
3
4
5
6
7
8
9
10
11
1.String tracPath=getExternalCacheDir()+"/myTrac.trace";
Debug.startMethodTracing(tracPath);//tracPath 生成文件的文件路径,需要读写权限
........
要测试的方法
......
2.Debug.stopMethodTracing();
3.生成文件在sd卡://tracPath=storage/emulated/0/Android/data/com.withub.android.cloudsharingcourt/cache/myTrac.trace
//如在application的oncreate()方法中第一行加入Debug.startMethodTracing("");,
//在oncreate()最后一行加入Debug.stopMethodTracing();
4.点击在androidStudio的右侧有个Device File explorer,这个工具是方便打开手机中的文件;
//根据传入的指定路径找到生成trace文件

trace文件打开后如下图:

traceView
traceView
traceView

底部4个菜单栏的含义:

1.CallChart:垂直方向是方法的调用栈(如A调用B则B在A的下面),另外系统API的颜色是橙色,应用自身的代码调用是绿色的,其他第三方的api调用则是蓝色;

image-20230918101722306

2.FlameChart:火焰图,收集相同调用顺序完全相同的函数,相对callChart作用要小很多;

image-20230918101758594

3.TopDowm:函数的调用列表,相当于CallChart的文字版;

选中方法点击右键可以点击Jump to Source跳转到对应代码;

可以查看指定方法的执行时间,Total=self+Children,self等于执行这个A方法的时间children则是A方法内其他方法的所用时间;

右上角有个下拉选项:Wall Clock Time(所在线程真正所用的时间),threadTime(cpu所执行的时间)

image-20230918101925336

4.BottomUp:和FlameChar类似重要性不高,显示的方法的被调用列表,点击B查看B被谁调用的;

image-20230918102230075

traceview的缺点

1
2
3
1.运行时开销严重,整体都会变慢
2.可能会带偏优化方向
3.traceview与cpu profiler(2者结合来分析)

systrace

1
2
3
4
5
6
7
8
9
10
11
12
1.结合Android内核的数据,生成Html报告
2.API18以上使用,推荐TraceCompat
3.使用方式
python systrace.py -t 10 [other-options] [categories]
//官方文档:https://developer.android.com/studio/commandline/systrace#command options

代码中埋点:
TraceCompat.beginSection("AppOncreate");
........
要测试的方法
......
TraceCompat.endSection();

脚本示例:

image-20230918104122465

-b 收集包的大小 ,-t 时间,-a 包名,-o 输出生成的文件名

systrace默认图:image-20230918104443838

image-20230918104519592

kernel:不同时间段只运行了4核,有时8核(如何高效的运用cpu);

image-20230918104829323

可以查看不同线程的运行情况,点击具体方法可以展开方法的详细运行时间

1
2
3
4
5
6
7
8
systrace优点:
轻量级,开销小
直观反映cpu利用率
注意:
cputime与walltime区别
- walltime是代码执行时间
- cputime是代码消耗cpu的时间(重点指标)
举例:锁冲突

四、优雅获取方法耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
常规方式
-背景:需要知道启动阶段所有方法耗时
-实现:手动埋点
-long time = System.currentTimeMillis();
-long cost = System.currentTimeMillis() - time;
- 缺点:侵入性强,工作量大

AOP介绍
-Aspect Oriented Programming,面向切面编程
-针对同一类问题的统一处理
-无侵入添加代码
AOP实战
-AspectJ使用(辅助实现AOP)
-classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0//旧版本在build.gradle buildScripe->dependencies中
-implementation 'org.aspectj:aspectjrt:1.8.+
-apply plugin: 'android-aspectjx//旧版本app build.gradle的顶部

-Join Points使用
-程序运行时的执行点,可以作为切面的地方
-函数调用、执行
-获取、设置变量
-类初始化

-PointCut
-带条件的JoinPoints

-Advice
-一种Hook,要插入代码的位置
-Before : PointCut之前执行
-After:PointCut之后执行
-Around:PointCut之前、之后分别执行

-语法简介
@Before("execution(*android.app.Activity.on**(..))"
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
-Before:Advice,具体插入位置
-execution :处理Join Point的类型,call、execution
-(*android.app.Activity.on**(..)):匹配规则
-onActivityCalled :要插入的代码

实战代码:任意自建的一个类image-20230918112134978

AOP的优点:无侵入性,修改方便

五、异步优化

优化小技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Theme切换:感官上变快了;(实际启动时间未变快)
1.
<layer-list xmIns:android="http://schemas.android.com/apk/res/androidandroid:opacity="opaque"><!-- The background color, preferably the same as your normal theme --><item android:drawable="@android:color/white"/><!-- Your product logo - 144dp color version of your app icon --><item> 4
<bitmap
android:src="@drawable/product_ogo_144dp'
android:gravity="center"/>
</item>
</layer-list>

2.
<activity ...android:theme="@style/AppTheme.Launcher”/>

3.
@Override
protected void onCreate(Bundle savedInstanceState){
// super.onCreate之前切换回来
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
}

异步优化

1
核心思想:子线程分担主线程任务,并行减少时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MyApp extends MultiDexApplication {
//同步工具类,用来协调多个线程之间的同步,用来作为线程间的通信而不是互斥作用
private CountDownLatch countDownLatch = new CountDownLatch(1);
@Override
public void onCreate() {
super.onCreate();
//动态获取设备核心数量,老版本生效新版本未验证;
int CORE_POOL_SIZE = Math.max(2,Math.min(Runtime.getRuntime().availableProcessors() - 1,4));
//创建线程池,java原生代码传入核心数;
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
//一个任务一个线程,多个任务则submit多个
executorService.submit(new Runnable() {
@Override
public void run() {
initTask1();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initTask2();
//initTask2执行完毕后通知countDownLatch;
countDownLatch.countDown();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initTask3();
}
});
...
try {
countDownLatch.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

异步优化注意

1
2
3
1.不符合异步要求
2.需要在某阶段完成
3.区分CPU密集型和IO密集型任务

六、异步优化启动器

常规异步痛点

1
2
3
1.代码不优雅
2.场景不好处理(依赖关系)
3.维护成本高

启动器介绍:

1
核心思想: 充分利用CPU多核,自动梳理任务顺序

启动器流程

1
2
3
1.代码Task化,启动逻辑抽象为Task
2.根据所有任务依赖关系排序生成一个有向无环图
3.多线程按照排序后的优先级依次执行

启动器流程图:image-20230918144000861

启动器起源码(启动器代码太多):https://gitee.com/cq_tyl/performance-optimization/tree/master(utils/TaskDispatcher.java)

1
2
3
4
5
6
7
8
9
10
示例:
TaskDispatcher.init( context:PerformanceApp.this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitAMapTask()).addTask(new InitStethoTask())InitweexTask())
.addTask(newaddTask(newInitBuglyTask())addTask(newInitFrescoTask())
.addTask(newInitJPushTask())
.addTask(newInitUmengTask())
.addTask(newGetDeviceIdTask())
.start();
dispatcher.await();//如有需要等待完成后才继续往下走

七、更优的延迟初始化的解决方案

常规方案:将延迟操作放到显示之后执行

1
2
3
4
5
1.New Handler().postDelayed
2.Feed展示后调用
缺点:
- 时机不便控制
- 导致Feed卡顿

更优方案:对延迟任务进行分批初始化

1
利用IdleHandler特性,空闲执行

实战源码:DelayInitDispatcher.java及其引用 (https://gitee.com/cq_tyl/performance-optimization/tree/master)

八、启动优化其他方案

优化总方针:

1
2
1.异步、延迟、懒加载
2.技术、业务相结合

注意事项

1
2
3
4
5
6
7
8
1.wall time与cpu time
2.cpu time才是优化方向
3.按照systrace及cpu time跑满cpu
4.监控的完善
- 线上监控多阶段时间(App、Activity、生命周期间隔时间 )
- 处理聚合看趋势
5.收敛启动代码修改权限
- 结合Ci修改启动代码需要Review或通知

其他方案

1
2
3
4
5
6
7
8
9
10
11
1.提前加载SharedPreferences
- Multidex之前加载,利用此阶段CPU
- 覆写getApplicationContext0)返回this
2.启动阶段不启动子进程
- 子进程会共享CPU资源,导致主进程CPU紧张
- 注意启动顺序:App onCreate 之前是ContentProvider
3.类加载优化:提前异步类加载
- Class.forName0只加载类本身及其静态变量的引用类
- new 类实例 可以额外加载类成员变量的引用类
4.启动阶段抑制GC
5.CPU锁频

九、问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.你做启动优化是怎么做的
- 分析现状、确认问题
- 针对性优化
- 长期保持优化效果
2.是怎么异步的,异步遇到问题没有
- 体现演进过程
- 详细介绍启动器
3.你做了启动优化,觉得有哪些容易忽略的注意点
- cpu time与wall time
- 注意延迟初始化的优化
4.版本迭代导致的启动变慢有好的解决方式吗
- 启动器
- 结合CI
- 监控完善
1.App性能概览与平台化实践
1
2
3
//性能优化
性能优化:包括了启动,内存,布局,卡顿,网络,电量,瘦身,稳定性;
性能监测平台:外网:听云,内网:自检各种性能检测机制,统一上报;

一、背景介绍

性能优化有哪些难题?

1
2
3
4
5
6
7
8
9
10
11
12
1.性能表现差
- App启动慢、卡顿、丢帧
- 内存占用高、抖动频繁
- 耗电、网络请求慢
- 崩溃率、异常率高
2.线上问题无从追查
- 如何保证异常感知灵敏度
- 如何复原"案发"现场
- 如何快速"止血"(修复)成功
3.性能优化的长期开销大
- 如何扼杀问题于萌芽
- 优化效果如何长期保持

相反的对性能优化要求

1
2
3
1.性能表现好
2.线上问题易追查
3.长期投入小

二、不同项目阶段,性能解决方案的演进

1
2
3
4
5
6
7
8
9
10
11
12
1.项目初期(快速开发阶段,快速占领市场的阶段)
只关心崩溃率、不采集性能数据
没有性能检测、优化方案
没有排查问题手段
2.项目壮大期(项目的发展和用户的增多时,逐渐的开始重视性能优化)
指标采集,不够全及深入
接入成熟APM,排查手段单一
线下检测、优化,方案不成型
3.项目成熟期(用户已达到数百万,重视性能优化)
重点关注性能问题,数据丰富,手段多样化
线上、线下一整套完善解决方案(重点)
自建APM,新产品可快速接入

线上线下各一套解决方案的原因

1
2
3
1.误区:对线上不重视
2.侧重点:线下预防、线上监控
3.方案不同:线下可用黑科技

为什么自建APM(应用性能监控平台)

1
2
3
1.成熟APM通用,但不满足个性化需求
2.外部APM与内部系统难打通,带来的时间成本
3.数据必须掌握在自己手中

三、业界优秀的平台

1
2
3
1.Crash收集平台
2.APM平台
3.自建解决方案

Crash收集平台

1
2
3
4
Bugly为代表
- 数据采集、上报成功率高
- 包含Java、Native崩溃
- 建议项目初期接入

APM平台

1
2
3
听云为代表
- 通用的性能解决方案,数据采集完善
- 方便接入,但不满足个性化需求,数据隐患

自建解决方案

1
2
3
美团、携程、360等
- 贴合自身业务特点,满足定制化需求
- 数据安全

四、问题

1
2
3
4
5
6
7
8
9
10
11
1.你们为什么要做性能优化
- 体验差影响核心指标
- 线上问题追查困难
- 降低性能优化的长期开销
2.介绍一下你们的性能平台
- 交代背景
- 具体讲解
3.你们为什么要自建APM
- 需求层面
- 效率层面
- 数据安全
3.1.leakcanary的使用

它是什么

一句话说,就是:

Android项目中,用于检测内存泄露,优化性能的工具

解决了什么问题

解决了,内存泄漏难以发现的问题。通过Leakcanary,可以轻松的找到GC中跟搜索法可达,然而不再使用的对象。
一般情况下,这种问题是很难被发现的,原因是他的出现是不经意间的,其中,内存泄露常见的场景有:

内存泄露常见场景

非静态内部类的静态实例

  1. 非静态内部类会持有外部类的引用
  2. 在外部类生命周期结束后,静态实例会长期维持着外部类的引用,导致无法被gc

多线程相关的匿名内部类\非静态内部类

  1. 匿名内部类同样也会持有外部类实例的引用,例如(AsyncTask、Tread、Runnable接口的实现类)
  2. 匿名内部类\非静态内部类中有耗时操作,在外部类生命周期结束后,仍然长时间维持着外部类的引用,导致无法被gc

Handler内存泄露

  1. Handler定义为非静态的,即持有了外部引用
  2. Message存储到MessageQueue中,耗时较长,无法被回收

怎样去使用它

集成进项目之中

  1. 新版本的Leakcanary只需添加依赖,无需更改代码

    1
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'

发现内存泄露

Leaknary 能够发现内存泄露的位置有:

  • 销毁的Activity 实例
  • 销毁的Fragment 实例
  • 销毁的View 实例
  • 销毁的ViewModel 实例
    LeakCanary可以hook到Android生命周期中,从而自动检测Activity和Fragment何时destroy,并进行垃圾收集。ObjectWatcher持有这些被destroy的对象的弱引用。通过以下方法,可以找到被Destroy的对象,例如
1
AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")

如果运行垃圾回收,并等待5秒钟后ObjectWatcher仍未清除 ,则认定可能发生内存泄漏。LeakCanary将此记录到Logcat:

img

在未被正常清楚的对象达到一定数量后,LeakCanary将其dump到存储堆,并显示通知:

img

图1.LeakCanary找到了4个未被正常清楚的对象。

注意:

当应用程序的状态是可见的时候,默认阈值为5 ,当应用程序是不可见的时候,默认阈值为1,。如果看到Leakcanary弹出通知,然后将应用程序压至后台(例如,直接上划进入主页),则LeakCanary阈值从5更改为1,并在5秒钟后dump到存储堆。若点击按钮将强制LeakCanary立即dump到存储堆。

打印堆中的信息

当保留对象的数量达到阈值时,LeakCanary将Java堆dump到存储在Android文件系统上的.hprof文件中(详见LeakCanary在何处存储堆转储?)。转储堆会使应用程序停止运行一小段时间,在此期间LeakCanary会显示Toast:

img

图2.LeakCanary显示了在堆放时的Toast

分析堆中的信息

LeakCanary.hprof使用Shark解析文件,并在该堆存储中找到保留的对象。

img

图3.LeakCanary在堆存储中查找保留的对象。

对于每个保留的对象,LeakCanary会查找引用路径,以防止对该保留的对象进行垃圾回收:至于泄露跟踪的具体内容,将在下一部分中展开:修复内存泄漏

img

图4.LeakCanary计算每个保留对象的泄漏跟踪

分析完成后,LeakCanary将显示带有摘要的通知,并在Logcat中显示结果。注意:下图将4个保留的对象归为2个不同的泄漏。LeakCanary为每个泄漏跟踪创建一个签名,并将具有相同签名的泄漏(即,由同一bug引起的泄漏)组合在一起。

img

图5. 4条泄漏迹线变成2个不同的泄漏特征。

点击弹出的通知,将提供更多详细的信息。也可以通过启动Leaks来找到该条内存泄露的详细信息

img

图6.LeakCanary为安装的每个应用程序添加一个启动器图标。

在Leaks中,每行对应一组具有相同签名的泄漏。应用程序首次出现的内存泄露类型将标记为“New”。

img

图7.将4个泄漏分组为2行,每个泄漏标记对应一个

点击泄漏将展示整个泄露的跟踪信息。img

图8. 显示了3个同一类别的内存泄露

泄漏签名是每个级联的散列参考怀疑导致泄漏,即,每个参考与红色下划线显示:img

图9.带有3个可疑参考的泄漏跟踪。

当泄漏跟踪以文本形式共享时,这些相同的可疑引用都带有下划线:如

img

并计算出响应的signature ,例如上述的签名为

img

分类内存泄露信息

常见的内存泄露Leaknary将其分成了第三方库和程序员操作失误两种。

  1. 第三方库的泄露如下所示:(带有Library Leak)
img

图10.LeakCanary发现了Library Leak。

2.程序员异常操作的泄露:

img

图10.NewsFragment.mRootV处泄露

如何解决内存泄露

  1. 我们在代码中能不用static变量持有context就不用,非要用就用weak引用。
  2. 对于内部类,尽量用静态内部类,这样就不会持有外部类引用。如果需要外部类引用做一些事,就手动赋给一个weak引用。
  3. 对于匿名内部类,不要图简单方便,实在不行就 写成外部类
  4. 在使用handler时,记得在activity的onDestroy()中加上remove(),有待尝试