目录:
Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。
这里举两个栗子来讲解一些CPU和GPU的作用:
那么,软件绘制和硬件绘制有什么区别呢?我们先看看下图:
这里软件绘制使用的是Skia库(一款在低端设备如手机上呈现高质量的 2D 图形的 跨平台图形框架)进行绘制的,而硬件绘制本质上是使用的OpenGl ES接口去利用GPU进行绘制的。OpenGL是一种跨平台的图形API,它为2D/3D图形处理硬件指定了标准的软件接口。而OpenGL ES是用于嵌入式设备的,它是OpenGL规范的一种形式,也可称为其子集。
并且,由于OpenGl ES系统版本的限制,有很多 绘制API 都有相应的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本的时候,还添加了对Vulkan(一套适用于高性能 3D 图形的低开销、跨平台 API)的支持。Vulan作为下一代图形API以及OpenGL的继承者,它的优势在于大幅优化了CPU上图形驱动相关的性能。
为了比较好的描述它们之间的作用,我们可以把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用分别如下:
在Android系统的显示过程中,虽然我们利用了GPU的图形高性能计算的能力,但是从计算Display到通过GPU绘制到Frame Buffer都在UI线程中完成,此时如果能让GPU在不同的线程中进行绘制渲染图形,那么绘制将会更加地流畅。
于是,在Android 5.0之后,引入了RenderNode和RenderThread的概念,它们的作用如下:
CPU将数据同步给GPU之后,通常不会阻塞等待RenderThread去利用GPU去渲染完视图,而是通知结束之后就返回。加入ReaderThread之后的整个显示调用流程图如下图所示:
在Android 6.0之后,其在adb shell dumpsys gxinfo命令中添加了更加详细的信息,在优化工具一节中我将详细分析下它的使用。
硬件加速相比软件绘制的几个优点:
硬件加速存在哪些问题:
我们都知道,硬件加速的原理就是将CPU不擅长的图形计算转换成GPU专用指令。
从时序图可知:
为什么要了解Android布局加载原理?
知其然知其所以然,不仅要明白在平时开发过程中是怎样对布局API进行调用,还要知道它内部的实现原理是什么。明白具体的实现原理与流程之后,我们可能会发现更多可优化的点。
我们都知道,Android的布局都是通过setContentView()这个方法进行设置的,那么它的内部肯定实现了布局的加载,接下来,我们就详细分析下它内部的实现原理与流程。
setContentView(getLayoutId())
点进去,发现是调用了AppCompatActivity的setContentView方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
这里的setContentView其实是AppCompatDelegate这个代理类的抽象方法:
/**
* Should be called instead of {@link Activity#setContentView(int)}}
*/
public abstract void setContentView(@LayoutRes int resId);
在这个抽象方法的左边,会有一个绿色的小圆圈,点击它就可以查看到对应的实现类与方法,在Androidx中,实现类是AppCompatDelegateImpl,实现方法如下所示:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
setContentView方法中主要是获取到了content父布局,也就是id是R.id.content的布局移除其内部所有视图之后并最终调用了LayoutInflater对象的inflate去加载对应的布局。
接下来,我们关注inflate内部的实现:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
这里只是调用了inflate另一个的重载方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 1
final XmlResourceParser parser = res.getLayout(resource);
try {
// 2
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在注释1处,通过Resources的getLayout方法获取到了一个XmlResourceParser对象,继续跟踪下getLayout方法:
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
这里继续调用了loadXmlResourceParser方法,注意第二个参数传入的为layout,说明此时加载的是一个Xml资源布局解析器。我们继续跟踪loadXmlResourceParse方法:
@NonNull
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
// 1
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
在注释1处,如果值类型为字符串的话,则调用了ResourcesImpl实例的loadXmlResourceParser方法。我们首先看看这个方法的注释:
/**
* Loads an XML parser for the specified file.
*
* @param file the path for the XML file to parse
* @param id the resource identifier for the file
* @param assetCookie the asset cookie for the file
* @param type the type of resource (used for logging)
* @return a parser for the specified XML file
* @throws NotFoundException if the file could not be loaded
*/
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
...
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
...
return block.newParser();
...
}
注释的意思说明了这个方法是用于加载指定文件的Xml解析器,这里我们之间查看关键的mAssets.openXmlBlockAsset方法,这里的mAssets对象是AssetManager类型的,看看AssetManager实例的openXmlBlockAsset方法做了什么处理:
/**
* {@hide}
* Retrieve a non-asset as a compiled XML file. Not for use by
* applications.
*
* @param cookie Identifier of the package to be opened.
* @param fileName Name of the asset to retrieve.
*/
/*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName)
throws IOException {
synchronized (this) {
if (!mOpen) {
throw new RuntimeException("Assetmanager has been closed");
}
// 1
long xmlBlock = openXmlAssetNative(cookie, fileName);
if (xmlBlock != 0) {
XmlBlock res = new XmlBlock(this, xmlBlock);
incRefsLocked(res.hashCode());
return res;
}
}
throw new FileNotFoundException("Asset XML file: " + fileName);
}
可以看到,最终是调用了注释1处的openXmlAssetNative方法,这是定义在AssetManager中的一个Native方法:
private native final long openXmlAssetNative(int cookie, String fileName);
与此同时,我们可以猜到读取Xml文件肯定是通过IO流的方式进行的,而openXmlBlockAsset方法后抛出的IOException异常也验证了我们的想法。因为涉及到IO流的读取,所以这里是Android布局加载流程一个耗时点
,也有可能是我们后续优化的一个方向。
分析完Resources实例的getLayout方法的实现之后,我们继续跟踪inflate方法的注释2处:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 1
final XmlResourceParser parser = res.getLayout(resource);
try {
// 2
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
infalte的实现代码如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
...
// 1
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 2
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
...
}
...
}
...
}
可以看到,infalte内部是通过XmlPull解析的方式对布局的每一个节点进行创建对应的视图的。首先,在注释1处会判断节点是否是merge标签,如果是,则对merge标签进行校验,如果merge节点不是当前布局的父节点,则抛出异常。然后,在注释2处,通过createViewFromTag方法去根据每一个标签创建对应的View视图。
我们继续跟踪下createViewFromTag方法的实现:
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
在createViewFromTag方法中,首先会判断mFactory2是否存在,存在就会使用mFactory2的onCreateView方法区创建视图,否则就会调用mFactory的onCreateView方法,接下来,如果此时的tag是一个Fragment,则会调用mPrivateFactory的onCreateView方法,否则的话,最终都会调用LayoutInflater实例的createView方法:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 1
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
// 2
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
...
}
...
// 3
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
}
...
}
LayoutInflater的createView方法中,首先,在注释1处,使用类加载器创建了对应的Class实例,然后在注释2处根据Class实例获取到了对应的构造器实例,并最终在注释3处通过构造器实例constructor的newInstance方法创建了对应的View对象。可以看到,在视图节点的创建过程中采用到了反射,我们都知道反射是比较耗性能的,过多的反射可能会导致布局加载过程变慢,这个点可能是后续优化的一个方向。
最后,我们来总结下Android中的布局加载流程:
从以上分析可知,在Android的布局加载流程中,性能瓶颈主要存在两个地方:
如果要获取每个界面的加载耗时,我们就必需在setContentView方法前后进行手动埋点。但是它有如下缺点
我们要使用AOP去获取界面布局的耗时,那么我们的切入点就是setContentView方法,声明一个@Aspect注解的PerformanceAop类,然后,我们就可以在里面实现对setContentView进行切面的方法,如下所示:
@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();
}
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}
为了获取方法的耗时,我们必须使用@Around注解,这样第一个参数ProceedingJoinPoint就可以提供proceed方法去执行我们的setContentView方法,在此方法的前后就可以获取setContentView方法的耗时。后面的execution表明了在setContentView方法执行内部去调用我们写好的getSetContentViewTime方法,后面括号内的*是通配符,表示匹配任何Activity的setContentView方法,并且方法参数的个数和类型不做限定。
上面我们使用了AOP的方式监控了Activity的布局加载耗时,那么,如果我们需要监控每一个控件的加载耗时,该怎么实现呢?
答案是使用LayoutInflater.Factory2,我们在基类Activity的onCreate方法中直接使用LayoutInflaterCompat.setFactory2方法对Factory2的onCreateView方法进行重写,代码如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件的加载耗时,
// 也可以做全局的自定义控件替换处理,比如:将TextView全局替换为自定义的TextView。
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();
// 1
View view = getDelegate().createView(parent, name, context, attrs);
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
// 2、setFactory2方法需在super.onCreate方法前调用,否则无效
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
}
这样我们就实现了利用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件加载耗时的处理,后续我们可以将这些数据上传到我们自己的APM服务端,作为监控数据可以分析出哪些控件加载比较耗时。当然,这里我们也可以做全局的自定义控件替换处理,比如在上述代码中,我们可以将TextView全局替换为自定义的TextView。
然后,我们注意到这里我们使用getDelegate().createView方法来创建对应的View实例,跟踪进去发现这里的createView是一个抽象方法:
public abstract View createView(@Nullable View parent, String name, @NonNull Context context,
@NonNull AttributeSet attrs);
在最新的Androidx中它对应的实现方法为AppCompatDelegateImpl对象的createView方法,代码如下所示:
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
这里最终又调用了AppCompatViewInflater对象的createView方法:
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
在AppCompatViewInflater对象的createView方法中系统根据不同的tag名字创建出了对应的AppCompat兼容控件。看到这里,我们明白了Android系统是使用了LayoutInflater的Factor2/Factory结合了AppCompat兼容类来进行高级版本控件的适配。
接下来,我们注意到注释1处,setFactory2方法需在super.onCreate方法前调用,否则无效,这是为什么呢?
这里可以先大胆猜测一下,可能是因为在super.onCreate()方法中就需要将Factory2实例存储到内存中。下面,我们就跟踪一下super.onCreate()的源码,看看是否如我们所假设的一样。AppCompatActivity的onCreate方法如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
第一行的delegate实例的installViewFactory()方法就吸引了我们的注意,因为它包含了一个敏感的关键字“Factory“,这里我们继续跟踪进installViewFactory()方法:
public abstract void installViewFactory();
这里一个是抽象方法,点击左边绿色圆圈,可以看到这里具体的实现类为AppCompatDelegateImpl,其实现的installViewFactory()方法如下所示:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
可以看到,如果我们在super.onCreate()方法前没有设置LayoutInflater的Factory2实例的话,这里就会设置一个默认的Factory2。最后,我们再来看下默认Factory2的onCreateView方法的实现:
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 1、First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// 2、If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
在注释1处,我们首先会尝试让Activity的Facotry实例去加载对应的View实例,如果Factory不能够处理它,在注释2处,就会调用createView方法去创建对应的View,AppCompatDelegateImplV9类的createView方法的实现上面我们已经分析过了,此处就不再赘述了。
1、代码动态创建View
使用Java代码动态添加控件的简单示例如下:
Button button=new Button(this);
button.setBackgroundColor(Color.RED);
button.setText("Hello World");
ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);
viewGroup.addView(button);
2、替换MessageQueue来实现异步创建View
在使用子线程创建视图控件的时候,我们可以把子线程Looper的MessageQueue替换成主线程的MessageQueue,在创建完需要的视图控件后记得将子线程Looper中的MessageQueue恢复为原来的。在Awesome-WanAndroid项目下的UiUtils的Ui优化工具类中,提供了相应的实现,代码如下所示:
/**
* 实现将子线程Looper中的MessageQueue替换为主线程中Looper的
* MessageQueue,这样就能够在子线程中异步创建UI。
*
* 注意:需要在子线程中调用。
*
* @param reset 是否将子线程中的MessageQueue重置为原来的,false则表示需要进行替换
* @return 替换是否成功
*/
public static boolean replaceLooperWithMainThreadQueue(boolean reset) {
if (CommonUtils.isMainThread()) {
return true;
} else {
// 1、获取子线程的ThreadLocal实例
ThreadLocal<Looper> threadLocal = ReflectUtils.reflect(Looper.class).field("sThreadLocal").get();
if (threadLocal == null) {
return false;
} else {
Looper looper = null;
if (!reset) {
Looper.prepare();
looper = Looper.myLooper();
// 2、通过调用MainLooper的getQueue方法区获取主线程Looper中的MessageQueue实例
Object queue = ReflectUtils.reflect(Looper.getMainLooper()).method("getQueue").get();
if (!(queue instanceof MessageQueue)) {
return false;
}
// 3、将子线程中的MessageQueue字段的值设置为主线的MessageQueue实例
ReflectUtils.reflect(looper).field("mQueue", queue);
}
// 4、reset为false,表示需要将子线程Looper中的MessageQueue重置为原来的。
ReflectUtils.reflect(threadLocal).method("set", looper);
return true;
}
}
}
3、AsynclayoutInflater异步创建View
在第三小节中,我们对Android的布局加载原理进行了深入地分析,从中我们得出了布局加载过程中的两个耗时点:
很明显,我们无法从根本上去解决这两个问题,但是Google提供了一个从侧面解决的方案:使用AsyncLayoutInflater去异步加载对应的布局,它的特点如下:
接下来,我将详细地介绍AsynclayoutInflater的使用。
首先,在项目的build.gradle中进行配置:
implementation ‘com.android.support:asynclayoutinflater:28.0.0’
然后,在Activity中的onCreate方法中将setContentView注释:
super.onCreate(savedInstanceState);
// 内部分别使用了IO和反射的方式去加载布局解析器和创建对应的View
// setContentView(R.layout.activity_main);
接着,在super.onCreate方法前继续布局的异步加载:
// 使用AsyncLayoutInflater进行布局的加载
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);
// findViewById、视图操作等
}
});
super.onCreate(savedInstanceState);
AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题:
Android AsyncLayoutInflater 限制及改进
4、使用掌阅团队推出的X2C进行布局加载优化
X2C框架保留了XML的优点,并解决了其IO操作和反射的性能问题。开发人员只需要正常写XML代码即可,在编译期,X2C会利用APT工具将XML代码翻译为Java代码。
X2C项目地址
但是,X2C框架还存在一些问题:
5、使用ConstraintLayout降低布局嵌套层级
1、使用异步布局框架Litho
Litho是Facebook开源的一款在Android上高效建立UI的声明式框架,它具有以下特点:
项目地址:
https://github.com/facebook/litho
Litho的缺点:
使用Litho,在布局性能上有很大的提升,但是开发成本太高,因为需要自己去实现很多的组件,并且其组件需要在编译时才能生成,不能够进行实时预览,但是可以把Litho封装成Flexbox布局的底层渲染引擎,以此实现上层的动态化,具体实现原理可参见Litho在美团动态化方案MTFlexbox中的实践。
2、使用Flutter实现高性能的UI布局
Flutter可以说是2019最火爆的框架之一了,它是 Google 开源的 UI 工具包,帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。对于Android来说,FLutter能够创作媲美原生的高性能应用,应用使用 Dart语言进行 开发。Flutter的架构类似于Android的层级架构,每一层都建立在前一层之上,其架构图如下所示:
在Framework层中,Flutter通过在 widgets 层组合基础 widgets 来构建 Material 层,而 widgets 层本身则是通过对来自 Rendering 层的低层次对象组合而来。而在Engine层,Flutter集成了Skia引擎用于进行栅格化,并且使用了Dart虚拟机。
那么Flutter的图形性能为何能够媲美原生应用呢?
接下来,我们以Flutter、原生Android、其它跨平台框架如RN来做比较,它们的图形绘制调用层级图如下所示:
可以看到,Flutter框架的代码完全取代了Java层的框架代码,所以只要当Flutter框架中Dart代码的效率可以媲美原生框架的Java代码的时候,那么总体的Flutter App的性能就能够媲美原生的APP。而反观其它流行的跨平台框架如RN,它首先需要调用自身的Js代码,然后再去调用Java层的代码,这里比原生和Flutter的App显然多出来一个步骤,所以它的性能肯定是不及原生的APP的。此外,Flutter App不同于原生、RN,它内部是直接包含了Skia渲染引擎的,只要Flutter SDK进行升级,Skia就能够升级,这样Skia的性能改进就能够同步到Flutter框架之中。而对于Android原生和RN来说,只能等到Android系统升级才能同步Skia的性能改进。
而Flutter又是如何实现高性能UI布局的呢?
接下来,我们来大致了解一下Flutter的UI绘制原理,它主要是通过VSYNC信号来使UI线程和GPU线程有条不紊的周期性的去渲染界面,其绘制原理图如下所示:
绘制步骤大致如下:
此外,Flutter 也采用了类似 Litho 的props属性不可变、Reat单向数据流的方案,用于将视图与数据分离。对于Flutter这一大前端领域的核心技术,笔者也是充满兴趣,后续会有计划对此进行深入研究,敬请期待。
3、使用RenderThread 与 RenderScript
在Android 5.0之后,Android引进了RenderThread,它能够实现动画的异步渲染。但是目前支持RenderThread完全渲染的动画,只有两种,即ViewPropertyAnimator和CircularReveal(揭露动画)。对于CircularReveal使用比较简单且功能较为单一,就不多做过多的描述了。下面我简单说下ViewPropertyAnimator中如何去利用RenderThread。
/**
* 使用反射的方式去创建对应View的ViewPropertyAnimatorRT(非hide类)
*/
private static Object createViewPropertyAnimatorRT(View view) {
try {
Class<?> animRtClazz = Class.forName("android.view.ViewPropertyAnimatorRT");
Constructor<?> animRtConstructor = animRtClazz.getDeclaredConstructor(View.class);
animRtConstructor.setAccessible(true);
Object animRt = animRtConstructor.newInstance(view);
return animRt;
} catch (Exception e) {
Log.d(TAG, "创建ViewPropertyAnimatorRT出错,错误信息:" + e.toString());
return null;
}
}
2、接下来,我们需要将ViewPropertyAnimatorRT设置给ViewPropertyAnimator的mRTBackend字段,这样ViewPropertyAnimator才能利用它去将动画交给RenderThread处理,如下所示:
private static void setViewPropertyAnimatorRT(ViewPropertyAnimator animator, Object rt) {
try {
Class<?> animClazz = Class.forName("android.view.ViewPropertyAnimator");
Field animRtField = animClazz.getDeclaredField("mRTBackend");
animRtField.setAccessible(true);
animRtField.set(animator, rt);
} catch (Exception e) {
Log.d(TAG, "设置ViewPropertyAnimatorRT出错,错误信息:" + e.toString());
}
}
/**
* 在animator.start()即执行动画开始之前配置的方法
*/
public static void onStartBeforeConfig(ViewPropertyAnimator animator, View view) {
Object rt = createViewPropertyAnimatorRT(view);
setViewPropertyAnimatorRT(animator, rt);
}
3、最后,在开启动画之前将ViewPropertyAnimatorRT实例设置进去即可,如下所示:
ViewPropertyAnimator animator = v.animate().scaleY(2).setDuration(2000);
AnimHelper.onStartBeforeConfig(animator, v);
animator.start();
现在,如果是做音视频或图像处理的工作,经常需要对图片进行高斯模糊、放大、锐化等操作,但是这里涉及大量的图片变换操作,例如缩放、裁剪、二值化以及降噪等。而图片的变换又涉及大量的计算任务,这个时候我们可以通过RenderScript去充分利用手机的GPU计算能力,以实现高效的图片处理。
而RenderScript的工作流程需要经历如下三个步骤:
1、你在做布局优化的过程中用到了哪些工具?
我在做布局优化的过程中,用到了很多的工具,但是每一个工具都有它不同的使用场景,不同的场景应该使用不同的工具。下面我从线上和线下两个角度来进行分析。
比如说,我要统计线上的FPS,我使用的就是Choreographer这个类,它具有以下特性:
同时,在线下,如果要去优化布局加载带来的时间消耗,那就需要检测每一个布局的耗时,对此我使用的是AOP的方式,它没有侵入性,同时也不需要别的开发同学进行接入,就可以方便地获取每一个布局加载的耗时。如果还要更细粒度地去检测每一个控件的加载耗时,那么就需要使用LayoutInflaterCompat.setFactory2这个方法去进行Hook。
此外,我还使用了LayoutInspector和Systrace这两个工具,Systrace可以很方便地看到每帧的具体耗时以及这一帧在布局当中它真正做了什么。而LayoutInspector可以很方便地看到每一个界面的布局层级,帮助我们对层级进行优化。
2、布局为什么会导致卡顿,你又是如何优化的?
分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿:
对此,我们的优化方式有如下几种:
3、做完布局优化有哪些成果产出?
对于Android的布局优化,笔者以一种自顶向下,层层递进的方式和大家一起深入地去探索了Android中如何将布局优化做到极致,其中主要涉及以下七大主题: