classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0’
项目gradle:
apply plugin: 'android-aspectjx’
implementation 'org.aspectj:aspectjrt:1.8.+’
针对Activity.setContentView监控简单示例:
@Aspect
public class PerformanceAop {
public static final String TAG = “aop”;
@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();
}
Log.i(TAG, name + " cost " + (System.currentTimeMillis() - time));
}
}
3.2 单个视图创建耗时监控:
Factory2、Factory本质上他俩就是创建View的一个hook,可以通过这个回调来监控单个View创建耗时情况。
注:Factory2继承自Factory,Factory2比Factory的onCreateView方法多一个parent的参数,即当前创建View的父View。
简单示例:
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
/
/1.配合getDelegate().createView来做高版本控件的兼容适配。
//2.单个View创建耗时统计。
long time = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
Log.i(“TAG”, name + " cost: " + (System.currentTimeMillis() - time));
return view;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
});
这里有一点要注意:setFactory2必须在super.onCreate(savedInstanceState)之前,不然会报如下错误:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.stan.topnews/com.stan.topnews.app.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3314)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3453)
打印结果:
2020-03-11 16:43:07.389 17078-17078/com.stan.topnews I/Perf: Connecting to perf service.
2020-03-11 16:43:07.567 17078-17078/com.stan.topnews I/perf: LinearLayout cost: 13
2020-03-11 16:43:07.569 17078-17078/com.stan.topnews I/perf: ViewStub cost: 0
2020-03-11 16:43:07.634 17078-17078/com.stan.topnews I/perf: TextView cost: 16
2020-03-11 16:43:07.637 17078-17078/com.stan.topnews I/perf: TextView cost: 3
…
3.3 布局绘制监控
这里用到的还是FPS,就监控一个doFrame。
简单示例:
private long mStartFrameTime = 0;
private int mFrameCount = 0;
/**
* 单次计算FPS使用160毫秒
/
private static final long MONITOR_INTERVAL = 160L;
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
/*
* 设置计算fps的单位时间间隔1000ms,即fps/s
*/
private static final long MAX_INTERVAL = 1000L;
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
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;
Log.i(TAG, “fps:” + fps);
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
}
});
}
});
}
FPS相关成熟三方库:
matrix 微信的卡顿检测方案,采用的ASM插桩的方式,支持fps和堆栈获取的定位,但是需要自己根据asm插桩的方法id来自己分析堆栈,定位精确度高,性能消耗小,比较可惜的是目前没有界面展示,对代码有一定的侵入性。如果线上使用可以考虑。
fpsviewer 利用Choreographer.FrameCallback来监控卡顿和Fps的计算,异步线程进行周期采样,当前的帧耗时超过自定义的阈值时,将帧进行分析保存,不影响正常流程的进行,待需要的时候进行展示,定位。
/ 布局加载优化 /
前面简单了解了布局加载流程,
性能瓶颈在于LayoutInflater.inflater过程,主要包括如下两点:
-
xmlPullParser IO操作,布局越复杂,IO耗时越长。
-
createView 反射,View越多,反射调用次数越多,耗时越长,但是这必须达到一定量级才会有明显影响。Java反射到底慢在哪?
那么很容易想到两个解决办法:要么把IO和反射交由子线程来处理,要么通过动态加载视图把IO和反射规避掉。那么市面上有没有相关的成熟方案呢?当然是有的,下面来简单看一看:
AsyncLayoutInflater
https://developer.android.com/reference/android/support/v4/view/AsyncLayoutInflater
AsyncLayoutInflater是google提供的方案,让LayoutInflater.inflater过程通过子线程来做:
new AsyncLayoutInflater(AsyncLayoutActivity.this)
.inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
实现也很简单:handle+thread+queue+inflater。可以理解为具有loop能力的子线程来实现的耗时部分异步处理。
这里有两点局限性:
-
不能设置LayoutInflater.Factory/Factory2
-
线程安全问题
详细源码分析和自定义AsyncLayoutInflater解决局限性问题可以参考如下文章,我就不重复造轮子了:
Android AsyncLayoutInflater 源码解析
https://www.jianshu.com/p/a3a3bd314c45
Android AsyncLayoutInflater 限制及改进
https://www.jianshu.com/p/f0c0eda06ae4
X2C
https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md
动态加载视图,这样能避免IO和反射,但是这样缺点是可读性差、可维护性差,因此掌阅团队开发的X2C做了鱼和熊掌都兼得的方案:X2C,它原理是采用APT(Annotation Processor Tool)+ JavaPoet技术来完成编译期间视图xml布局生成java代码,这样布局依然是用xml来写,编译期X2C会将xml转化为动态加载视图的java代码。
这里个人理解可能存在的局限性:
-
失去系统兼容AppCompat
-
是不是能全面支持所有布局属性及自定义属性
-
如果视图全部用X2C来处理,会造成代码冗余。
/ 布局绘制优化 /
这部分是由ViewRootImpl触发的performTraversals,它主要包含:measure(确定ViewGroup以及View的大小) layout(ViewGroup决定View的摆放位置) draw(绘制视图)三个部分。另外,绘制好的DisplayListOp tree最终需要经过OpenGL命令转换交由GPU渲染,如果同一个像素点被多次重复绘制,势必也是造成浪费以及GPU任务变重。
因此布局绘制最终优化方向就是如下两个:
5.1 优化布局层级及其复杂度
measure、layout、draw这三个过程都包含的自顶向下的view tree遍历耗时,它是由视图层级太深会造成耗时,另外也要避免类似RealtiveLayout嵌套造成的多次触发measure、layout的问题。最后onDraw在频繁刷新时可能多次被触发,因此onDraw不能做耗时操作,同时不能有内存抖动隐患等。
优化思路:
-
减少View树层级
-
布局尽量宽而浅,避免窄而深
-
ConstraintLayout 实现几乎完全扁平化布局,同时具备RelativeLayout和LinearLayout特性,在构建复杂布局时性能更高。
-
不嵌套使用RelativeLayout
-
不在嵌套LinearLayout中使用weight
时可能多次被触发,因此onDraw不能做耗时操作,同时不能有内存抖动隐患等。
优化思路:
-
减少View树层级
-
布局尽量宽而浅,避免窄而深
-
ConstraintLayout 实现几乎完全扁平化布局,同时具备RelativeLayout和LinearLayout特性,在构建复杂布局时性能更高。
-
不嵌套使用RelativeLayout
-
不在嵌套LinearLayout中使用weight