得物布局构建耗时优化方案实践

一、背景

当谈到移动应用程序的体验时,页面启动速度是其中至关重要的一点,更快的页面展示速度确保应用程序可以迅速加载并响应用户的操作, 从而提高用户使用 App 时的满意度。在页面启动的整个流程中,随着 UI 复杂度的上升,布局的 Inflate 耗时占据了相当一部分关键的比例,本文分享得物自身在页面布局构建耗时优化方案上的探索历程。

二、现有方案

在布局构建耗时优化上,开源社区上有一些现成的方案可供参考,我们首先看下目前一些已知的技术方案。

掌阅X2C

掌阅的 X2C 方案开源于 2018 年,其通过 APT 在编译期间对目标 XML 文件进行解析,并翻译成 XML View 树结构对应的 Java 文件。比如以下的布局 XML 文件。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingLeft="10dp"><includeandroid:id="@+id/head"layout="@layout/head"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerHorizontal="true" /><ImageViewandroid:id="@+id/ccc"style="@style/bb"android:layout_below="@id/head" />
</RelativeLayout>

转换成 Java 文件:

public class X2C_2131296281_Activity_Main implements IViewCreator {@Overridepublic View createView(Context ctx, int layoutId) {Resources res = ctx.getResources();RelativeLayout relativeLayout0 = new RelativeLayout(ctx);relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);view1.setLayoutParams(layoutParam1);relativeLayout0.addView(view1);view1.setId(R.id.head);layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);ImageView imageView2 = new ImageView(ctx);RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));imageView2.setLayoutParams(layoutParam2);relativeLayout0.addView(imageView2);imageView2.setId(R.id.ccc);layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);return relativeLayout0;}
}

344.png

优点:

  • 性能高,没有了加载 XML 的 IO 和递归解析过程。
  • 避免了类反射构建的耗时。
  • 基于 APT 直接生成 Java 文件。

缺点:

  • View 兼容性差,适配成本高,自定义 View 需要配置属性对应的方法。
  • 功能不完整,不支持 Merge 标签,无法查询系统 style,所以只支持应用内 style。
  • 由于 APT 本身的特性,在 XML 发生变化时,对应注解处理器生成的 Java 构建文件不会同步发生变, 对于不熟悉的同学来说容易踩坑。

AsyncLayoutInflater

AsyncLayoutInflater 是由 Android Google 官方提供的异步 Inflate API,其主要思路是将 Inflate 操作放在异步线程并行操作,从而让主线程可以继续执行一些其他的初始化操作,通过异步回调在相应的 Layout View 创建完成后,再设置到页面上。

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);new AsyncLayoutInflater(this).inflate(R.layout.async_layout,null,new AsyncLayoutInflater.OnInflateFinishedListener() {@Overridepublic void onInflateFinished(View view, int resid, ViewGroup parent) {setContentView(view);}});
}

优点:

  • 将 UI 加载过程迁移到了子线程,保证了 UI 线程的高响应。
  • 不存在 View 兼容性问题。

缺点:

  • 有一定改造成本,在原有的页面直接引入 AsyncLayoutInflater 进行改造时,由于从同步调用改成异步回调调用导致的逻辑结构变化容易引入 NPE 之类的风险。

  • 内部依然存在部分 View 的反射需要创建的开销。

ViewCompiler

Google 加入了一个 ViewCompiler,从原理来看是系统在安装 APK 的时候自动对布局文件做的编译优化,ViewCompiler 会将可优化的 XML 布局转化为代码构建的代码,并编译成 Dex 文件。

333.png

之后在程序运行时,首次使用 Infalter 类时,就会提前加载该 Dex 文件。

888.png

之后在调用 Infalte 函数 Inflate相应布局资源时,会尝试调用优化后的 pacakgeme.CompileView 类的 Infalte 函数,直接生成对应的 View。

099.png

088.png

ViewCompiler 编译 Layout 的原理其实和现有的 XML To Code 方案是类似的,都是解析 Layout XML 文件,再根据 XML 节点信息生产组装 View 的代码。只不过在应用层我们的方案是提前编译生成 Java 或 Class 文件,而系统是直接编译生成 Dex 文件。

077.png

ViewCompiler 虽然在 Android Q Beta 2 的时候被添加进来,但到目前为止仍是一个实验性质的东西,默认情况下应用程序都是无法使用到的。

019.png

三、得物自研X2C框架实践

针对以上问题,我们决定构建得物自研的 X2C 框架。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"app:x2c="standard"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/tv_1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="测试一下效果"android:textSize="24sp" /><com.shizhuang.x2c.CustomViewandroid:layout_width="50dp"android:layout_height="50dp"android:layout_gravity="center"app:mixColor="black" />
</merge>

生成 XML2Code 代码为:

@X2CRes(R.layout.activity_test2)
public class activity_test2 implements IViewFactory {@Overridepublic View createView(Context themeContext,ViewGroup parentView,boolean attachToParent,AttributeSet parentAttributeSet) {if (parentView == null) {throw new X2CException("parentView is null when root is merge");}XmlResourceParser parser = themeContext.getResources().getXml(R.layout.activity_test2);AttributeSetHelper.nextAttributeSet(parser);AttributeSet attrs9 = AttributeSetHelper.nextAttributeSet(parser);TextView view9 = new TextView(themeContext, attrs9);parentView.addView(view9, parentView.generateLayoutParams(attrs9));ViewAccessHelper.notifyFinishInflate(view9);AttributeSet attrs16 = AttributeSetHelper.nextAttributeSet(parser);CustomView view16 = new CustomView(themeContext, attrs16);parentView.addView(view16, parentView.generateLayoutParams(attrs16));ViewAccessHelper.notifyFinishInflate(view16);return parentView;}@Overridepublic String layoutName() {return "activity_test2";}
}

View构建流程解析

生成 AttributeSet

在生产 AttributeSet 的探索上,我们首先研究系统的 LayoutInfalter 是如何生成 AttributeSet 的,通过对源码分析后发现,AttributeSet 是一个抽象接口,其唯一的直接子类是 XmlResourceParser。

322.png

系统的 LayoutInflater 的构建过程中,首先通过 Resources 生成对应布局文件的 XmlResourceParser。

778.png

由于 Parser 继承自 AttributeSet,因此可以将 Parser 强转为 Attributeset,之后先生成 RootView,再调用 RInfalteChildren 构建所有的子 View。

667.png

而在子 View 的构建过程中,使用的还是一开始从 XmlPullParser 转换的 AttributSet,这里的 XmlPullParser 和 AttributeSet 其实是同一个对象,XmlPullParser 解析二进制 XML 采用的是 SAX 方式,即边读边解析, 通过不断调用 Next 函数,在构建对应节点的 View 时,读取当前的 AttributeSet 信息。

646.png

创建 View 的方式

1122.png

View 实例的创建有两种方式:

第一种是类似掌阅 X2C 的方式, 直接调用目标 View(Context Context) 构造函数创建,此时还需要生成额外的属性设置 API,如 SetWidth,对于自定义的属性需要做专门的适配处理。

第二种是调用 View(Context Context, AttributeSet Attrs) 构造函数,LayoutInflater 内部解析 XML 并构建相应 View时,调用的就是这个构造函数。

因此,从兼容性的角度上考虑,采用第二种方式构建更为合理,剩下的问题就转化为如何生成对应布局文件中对应 View 的 AttributeSet。

生成 LayoutParams

AttributeSet 除了用于构建当前节点的 View 以外,还用于构建 LayoutParams。

2232.png

LayoutParams 的构建同时还依赖于当前节点的夫容器 Parent,不同的容器生成不同的 LayoutParams,例如 FrameLayout.LayoutParams、LinearLayout.LayoutParams 等。

Merge 和 Include 标签

Merge 标签跟普通标签的区别在于,Merge 标签是一个虚拟根节点。Merge 是为了降低 View 嵌套层级设计的,所以 Merge 标签为根节点的布局是没有根 View 的,所以也无法返回布局根 View,只能将参数的 ViewParent 返回。

116.png

Merge 标签需要搭配 Include 标签使用,但是 Include 标签却并不是只能搭配 Merge 标签。所以在解析 Include 标签的 Layout 的时候,我们并不知道包含过来的是普通布局还是 Merge 布局。

022.png

但是普通布局和 Merge 布局的实现并不一样。

对于 Include 普通布局,逻辑要复杂的多。Include 标签本身有 AttributeSet 信息,包含的布局根节点也有 AttributeSet 信息,应该使用哪个呢?构建根 View 的时候,使用根节点的 AttributeSet,但是在 View 构建完成后,需要将 Include 标签属性中的 Android:ID 和 Android:visiablity 属性赋值给根 View。

990.png

在生成根 View 的 LayoutParams 的时候,优先使用 Include 标签的 AttributeSet,如果生成失败再使用根节点的 AttributeSet。

117.png

插件选型:APT or Gradle Plugin?

APT 方式的问题

在 XML 生成代码构建的实践过程中,我们一开始也是采用的掌阅 X2C 的方案,在业务代码中插入如下注解,用于标记需要转换成 Java 的 XML 文件,在各业务模块中注册注解处理器,直接生成对应的 Java 源代码。

@Xml("activity_test2")

最后发现这样的方式会带来不小的问题:

  • APT 的编译 Target 是 Java 源代码, 所以在只有 XML 文件变更时,并不会自动重新生成新的 Java 布局代码。这样一次 XML 修改,在转换成 Java 代码的时候,就被编译系统忽略了。

  • 使用 XML 注解标注文件名的方式,并没有让注解跟文件本身绑定。当文件改名的时候,这个注解并不能感知,文件的修改者也无法感知到有这么一个跟文件没有直接关系的文件名注解。

  • 得物采用的是多仓库多模块开发,壳工程引入子工程的依赖,最后是以 AAR 二进制依赖的方式进行构建。每个模块接入的 X2C 插件版本不同,因此构建出的产物也不同,这会导致 X2C 版本碎片化严重。容易出现生成之前生成的 View 构建代码和最新的运行时 X2C-SDK 不兼容的问题,也增加了 X2C-SDK 后续升级过程中的维护困难。

使用 AGP 统一构建

我们最终采用的通过 AGP 插件,在壳工程对所有目标 XML 进行统一构建的方式。

0334.png

在 Android 工程的编译过程中,ProcessResources 任务将所有依赖的模块的资源进行处理,生成 Resources.ap_ 文件和 R 文件。Resources.ap_ 是资源压缩包,里面的 XML 资源是已经被编译成二进制格式的资源。

X2C-AGP 的核心功能主要有两部分:

  • GenerateJavaTask 是将 XML 布局文件 转换成 Java 布局代码。

  • ExcludeTransform 后续介绍。

我们约定当布局 XML 文件中,添加了自定义属性 app:x2c 时,表示该文件需要进行 X2C 构建代码生成。GenerateJavaTask 任务遍历 Resources.ap_ 文件,将包含该自定义属性的布局文件转换成 Java 代码。还生成了 Resource ID 到 Java 布局类的映射关系。

public class X2CResPool {public static IViewFactory getFactoryBy(int layoutRes) {switch(layoutRes) {case R.layout.activity:return new activity();case R.layout.activity_main_du2:return new activity_main_du2();case R.layout.activity_test1:return new activity_test1();case R.layout.activity_test2:return new activity_test2();case R.layout.merge_activity:return new merge_activity();default:return null;}}
}

壳工程通过任务 GenerateJavaTask 将二进制 XML 布局文件,转换成 Java 布局代码时。Java 布局代码中使用了很多自定义 View。这些自定义 View 是在业务模块中定义的,而在壳工程的 App 模块中,由于并没有显示申明对应 View 的模块依赖,会导致编译 Java 布局文件时出现类未找到的问题,导致编译失败。而如果人手动去解决该问题,为 App 模块添加相应 View 的模块依赖,显得较为繁琐。每次增加一个需要支持 X2C 的 XML 文件的时候,都需要增加壳工程的工程依赖关系,且自定义 View 到底在哪个模块也不不是这么一目了然。

一个解决方案是不再生成 Java 源码,直接生成 Java 字节码,这样可以绕过编译依赖。直接生成字节码的方案增加了项目的升级和维护成本,且不便于业务侧同学验证生成的 Java 布局代码是否正确。

另一种方案是在壳工程重新实现一次依赖的自定义 View,这样就造成了 APK 中会有重复的类,所以需要在 Transform 阶段将重复的 View 去掉,ExcludeTransform 就是完成这个任务的。壳工程中实现的自定义 View 会有 @X2CResTemp 注解,在 ExcludeTransform 中,通过 ASM 遍历工程中所有字节码,将有 @X2CResTemp 注解的类从编译系统中删除。

如何在壳工程中实现依赖的自定义 View 呢,观察生成的 Java 代码,会发现我们只用了自定义 View 的构造函数,并不需要实现一个完整的自定义 View,只要有构造函数,就可以在编译阶段通过了。

@X2CResTemp
public class CustomView extends ViewGroup {public CustomView(Context context, AttributeSet attributeSet) {super(context, attributeSet);}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {}
}

预加载

优化布局的加载性能,除了 X2C 方案以外,预加载是一个效果更为显著的功能。在 Androidx 中已经有提供了 AsyncLayoutInflater 用于进行 XML 的异步加载,在这个类基础上可以封装一个异步预加载工具,但是实际使用下来会发现直接使用 AsyncLayoutInflater 很容易出现锁的问题,甚至导致了更多的耗时。通过分析我们发现,这是因为在 LayoutInflate 中存在着对象锁,并且即使通过构建不同的 LayoutInflate 对象绕过这个对象锁,在 AssetManager 层、Native 层仍然会有其他锁。

预加载时机

布局预加载存在于两个时机:

  • App 启动时,Application 的 OnCreate 阶段,可以对首页布局进行预加载。

  • 打开新的 Activity 前,预加载这个 Activity 的布局。

在 App 启动阶段对主页的布局文件进行预加载,统一放到启动任务加载中去做。新的 Activity 启动之前,如何做布局预加载呢?打开新的 Activity 的场景可能十分多,难道需要在每个 StartActivity 调用之前都插入一段预加载布局的代码吗,且打开新的 Activity 的地方需要能获取布局资源 ID。

答案是跟路由结合在一起,ARouter 提供了路由拦截器,不同的业务模块,可以在模块中使用注解注册一个 ARouter 路由拦截器,并在拦截器中自定义自身模块内页面的预加载策略,如下:

@Interceptor(priority = 1)
class X2CPreloadInterceptor : IInterceptor {private lateinit var applicationContext: Contextoverride fun init(context: Context) {applicationContext = context.applicationContext}override fun process(postcard: Postcard, callback: InterceptorCallback) {if (postcard.path == CommunityRouterTable.FEED_DETAILS_PAGE) {X2CUtil.preload(applicationContext, R.layout.du_trend_detail_fragment_trend_details_tab)X2CUtil.preload(applicationContext, R.layout.du_trend_detail_fragment_trend_details)}callback.onContinue(postcard)}
}

所有打开新 Activity 的场景都需要使用路由,所以在路由拦截器中能收敛打开新 Activity 的场景。

Context 及主题适配

对 Activity 的布局文件进行预加载的时候,Activity 还没有创建,所以我们无法拿到 Activity 的 Context。但是构建 View 需要 Context,所以我们使用 Application 的 Context 代替。但是很多业务侧拿着 View 的 Context 当 Activity 用的场景,为了兼容这种场景,所以在预加载的 View 被添加到 ViewTree 前需要将 ApplicationContext 替换成 Activity 的 Context。

7.png

View 没有提供替换 Context 的 API,所以使用反射替换 mContext 成员的值。

733.png

如此这般,大部分场景下已经没有什么问题了,但是仍然遇到了新的问题

java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).at com.shizhuang.x2c.task.DeferredRunnable.run(deferred.kt:56)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)at com.shizhuang.duapp.common.base.delegate.tasks.optimize.startupoptimize.DefaultThreadFactory$newThread$1.run(X2CInitTask.kt:82)at java.lang.Thread.run(Thread.java:920)
Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:243)at com.google.android.material.internal.ThemeEnforcement.checkAppCompatTheme(ThemeEnforcement.java:213)at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:148)at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:76)at com.google.android.material.tabs.TabLayout.<init>(TabLayout.java:474)at com.google.android.material.tabs.TabLayout.<init>(TabLayout.java:454)at com.shizhuang.x2c.res.layout.du_trend_detail_fragment_trend_details_tab.createView(du_trend_detail_fragment_trend_details_tab.java:88)at com.shizhuang.x2c.inflate.IViewFactory$DefaultImpls.createView$default(IViewFactory.kt:9)at com.shizhuang.x2c.X2CUtil$preload$3$1$1.invoke(X2CUtil.kt:150)at com.shizhuang.x2c.X2CUtil$preload$3$1$1.invoke(X2CUtil.kt:149)at com.shizhuang.x2c.task.DeferredRunnable.run(deferred.kt:49)... 4 more

这是因为布局使用了跟主题相关的内容,Application 的 Context 没有主题信息,所以预加载的 Context 需要加上布局文件所属的 Activity 的主题,如下:

X2CUtil.preload(applicationContext.withTheme(R.style.FeedDetailsActivity), R.layout.du_trend_detail_fragment_trend_details_tab)

构建线程优先级调优

在框架开发完成后,我们在得物首页场景下进行了框架接入,在 Application 的 onCreate 阶段对 HomeActivity 的布局进行了相应布局的预加载。对预加载进行线下测试,线下数据表现较好。在开启预加载的时候,秒开数据显著好于无预加载场景。然而预加载功能上线后,线上 AB 统计的平均耗时数据确令人不解,在开启预加载情况下,首页布局加载耗时竟然大于无预加载情况,分析了样本数据后,发现在异步线程构建存在的异常耗时样本远远多于在主线程构建的样本数量。

88.png

我们在线下针对线上容易出现异常耗时的设备进行了复测,发现确实存在类似的情况,此时我们联想到 Android 系统在对 SharedPrefenrece 做的一个优化,由于异步线程的优先级默认比主线低,因此在 Activity onStop 的时候,系统会把异步线程 SP 未完成同步的任务直接取出到主线程执行,异步构建是不是也是由于线程优先级导致异步构建时无法获取到充足的 CPU 时间片导致的,最终我们在线下打印了主线程和异步线程执行时获取的 CPU 时间片占比,验证了该猜想。

55.png

可以看到,虽然提前进行了异步构建的工作,但是到页面需要使用对应 View 的时候,异步构建的任务还没有完成,因此主线程只能进行等待,并且由于异步线程优先级较低,出现了一个高优先级的线程等待另一个低优先级线程的情况,并且优先级导致的时间片分配的原因,这里的等待其实不如直接在主线程直接重新构建。异步 View 构建线程其实是为主线程服务的,我们需要提高对应工作线程的优先级。

Android 设置线程优先级的方法有两种:

  • Java API 使用 Thread 类的 setPriority,值为 0~10,值越大,优先级越高,所能获取的时间片越多。
  • Android 系统使用 Process 类的 setThreadPriority,值为 -20~20,值越小,优先级越高,所能获取的时间片越多。

在 Android 中,无论通过什么方式设置的线程优先级,其实本质上都是通过 Native 层,设置 Nice 的值来实现的。线程优先级必须在线程创建成功后,才能设置,因为线程创建完成后,才能拿到线程 ID。注意 Thread 的 Start 方法执行后,线程不一定创建完成,Thread 的 Runnable 开始执行才能认为创建完成。

线程默认优先级为 0,主线程默认为-20,部分 ROM 的主线程默认-10。我们将预加载线程优先级提升为-16。

private class DefaultThreadFactory : ThreadFactory {private val number = AtomicInteger(0)override fun newThread(r: Runnable): Thread {return Thread(null, Runnable {Process.setThreadPriority(-16)r.run()}, "X2C-Thread${number.incrementAndGet()}")}
}

经过调整后,性能提升显著,在对应页面需要获取 View 时,异步任务基本已经提前完成。

93.png

多线程构建探索

默认情况下,一个 View 树的构建是单线程的,即总是从 ViewRoot 层级向下构建,无论采用现有的哪种方案,最终构建的总耗时总是大于每个 View 构建耗时之和,无法利用多线程的优势缩减 View 构建耗时。

71.png

为了进一步提升预加载的效率,我们考虑使用多线程对预加载进行性能提升。布局的加载受限于 XML 的解析,XML 的解析只能使用单线程。对二进制 XML 文件格式进行研究,看看是否有进一步优化的可能性。

自己生成 AttributeSet

通过 XmlResourceParser 获取 AttributeSet 是实现成本较低的方式,但它存在以下问题:

  • 仍需要 XML 文件的存在,通过 Resource 读取二进制 XML 资源,涉及到一部分文件 IO。
  • XmlResourceParser 对 XML 文件读取是 Pull 模式,如果我们计划对 ViewTree 的构建过程进行多线程构建优化,无法直接获取对应节点的 AttributeSet 信息。

因此,我们进行了自己生成 AttributSet 的探索,首先,XmlBlock 的生成,除了类似 LayoutInflater 构建过程中直接传入 LayoutID 的方式(如下)。

22.png

也可以直接传入对应的 Byte[] 进行生成, 因此,我们如果可以直接生成 XML 文件中各个 View 属性信息对应的二进制文件,就可以直接通过 XMLBlock 构建对应的 AttributeSet。

35.png

211.png

二进制 XML 重组

二进制的 XML 文件其内容结构如下:

122.png

二进制 XML 有以下 6 部分组成:

  1. 文件头
  2. 字符串常量池
  3. 系统资源 ID 池
  4. Start NameSpace Chunk
  5. 嵌套的节点 Chunks
  6. End NameSpace Chunk

二进制 XML 保留了文本 XML 中节点的嵌套结构关系。XML 的节点之间除了用嵌套结构来描述父子关系外,父子之间没有信息依赖,子节点的解析不依赖于任何父节点信息。父子节点的信息解析是可以完全独立的,所以我们在解析文件之前,将完整的 XML 文件按节点拆成每个 N 个独立的文件,文件格式如下:

  1. 文件头
  2. 字符串常量池
  3. 系统资源 ID 池
  4. Start NameSpace Chunk
  5. 节点 Chunk
  6. End NameSpace Chunk

    335.png

文件重组后,每个文件的 File Size 字段需要重新计算。二进制数据保存在代码中,用函数分割保存。

class activity_test2 {public byte[] xmlHeader() {return new byte[] {FileHeader+StringPool+ResourcesIdPool+StartNamespaceChunk}}public byte[] tag1() {return new byte[] {StartTagChunk:LinearLayout+EndTagChunk:/LinearLayout}}public byte[] tag2() {return new byte[] {StartTagChunk:TextView+EndTagChunk:/TextView}}public byte[] tag3() {return new byte[] {StartTagChunk:Button+EndTagChunk:/Button}}public byte[] xmlEnd() {return new byte[] {EndNamespaceChunk}}
}

留待进一步

多线程加载方案对单个 XML 的预加载性能有所提升,但是因为预加载主要是在 App 启动的时候使用,这个时候影响性能的并不是线程不够,而是 CPU 性能不够。同时 App 启动阶段预加载的资源不是只有一个,而是多个。多线程主要是拉平了各个线程的算力消耗。

332.png

实现多线程方案,也引入了新的问题:

  • 让 X2C 的实现变的复杂了,兼容多线程方案的实现性能相对不兼容多线程的下降了。
  • 多线程方案依赖于对二进制 XML 进行重组,代码中多拷贝了一份资源。

四、线上性能收益

以首页的启动速度为例。

这里的启动速度标准是,从首页Actiivty 的 onCreate 开始执行到 onResume 函数执行结束。

  • LOCAL: 表示未做任何优化的数据 ,平均耗时 292ms。
  • X2C: 未做预加载,但使用了X2C的infalte构建, 平均耗时 267ms。
  • CACHE: 进行了提前预加载,平均耗时 216ms。

    554.png

以 社区容器 页面的启动速度为例。

  • LOCAL: 平均耗时 293ms。
  • X2C: 平均耗时 210ms。
  • CACHE: 平均耗时 150ms。

    556.png

五、框架对比

305.png

六、结论

通过实践上述优化方案,可以显著减少布局构建的耗时,提高应用的性能和用户体验。本次项目经过三轮的优化迭代,整个技术迭代过程中,一个核心的理念就是数据驱动,一切的优化都要以数据的提升来作为标准,遇到问题解决问题。

本次技术优化最初的切入点是 XML2Code,但是在进行线上验证后,发现仅仅只是 XML2Code 并不能达成我们预期的结果。于是整个项目回归到了更高层级的目标上 —— 优化布局构建耗时。为了进一步优化布局构建的耗时,预加载、多线程构建,可谓“无所不用其极”,最后达成预期结果。

所以盯住结果,不要拘泥于什么具体的技术!

*文/令古

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/741155.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Java探索之旅】解密Java中的类型转换与类型提升

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java编程秘籍 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一、类型转化1.1 自动类型转换&#xff08;隐式类型转换&#xff09;1.2 强制类型转换…

Arduino IDE的下载和安装

一、Arduino的介绍 Arduino是一款开源电子原型平台&#xff0c;主要包含两部分&#xff1a;硬件&#xff08;各种型号的Arduino板&#xff09;和软件&#xff08;Arduino IDE&#xff09;。这个平台由意大利的Massimo Banzi、David Cuartielles等人共同开发设计&#xff0c;并于…

ES分片均衡策略分析与改进

从故障说起 某日早高峰收到 Elasticsearch 大量查询超时告警&#xff0c;不同于以往&#xff0c;查看 Elasticsearch 查询队列监控后发现&#xff0c;仅123节点存在大量查询请求堆积。 各节点查询队列堆积情况 查看节点监控发现&#xff0c;123节点的 IO 占用远高于其他节点。…

【论文阅读】IEEE Access 2019 BadNets:评估深度神经网络的后门攻击

文章目录 一.论文信息二.论文内容1.摘要2.引言3.主要图表4.结论 一.论文信息 论文题目&#xff1a; BadNets: Evaluating Backdooring Attacks on Deep Neural Networks&#xff08;BadNets:评估深度神经网络的后门攻击&#xff09; 论文来源&#xff1a; 2019-IEEE Access …

安卓内嵌uniapp的H5页面 android调用h5内部方法

uniapp H5与原生安卓的数据互通和方法调用_安卓代码中调用uniapp中页面中的方法-CSDN博客文章浏览阅读8.7k次&#xff0c;点赞4次&#xff0c;收藏22次。1、准备我这里是uniapp与原生安卓之间的相互调用&#xff0c;也就是原生安卓内嵌H5页面&#xff0c;下面先来准备一下安卓端…

吴恩达 x Open AI ChatGPT ——如何写出好的提示词视频核心笔记

核心知识点脑图如下&#xff1a; 1、第一讲&#xff1a;课程介绍 要点1&#xff1a; 上图展示了两种大型语言模型&#xff08;LLMs&#xff09;的对比&#xff1a;基础语言模型&#xff08;Base LLM&#xff09;和指令调整语言模型&#xff08;Instruction Tuned LLM&#xff0…

CentOS 7 devtoolset编译addressSanitizer版本失败的问题解决

在我的一个Cent OS7开发环境中&#xff0c;按https://yeyongjin.blog.csdn.net/article/details/134178420的方法升级GCC版本到8.3.1。 这两天&#xff0c;要用Google的addressSanitizer检验内存问题&#xff0c;加上编译参数后&#xff0c;却发现编译不通过。configure时直接退…

WanAndroid(鸿蒙版)开发的第二篇

前言 DevEco Studio版本&#xff1a;4.0.0.600 WanAndroid的API链接&#xff1a;玩Android 开放API-玩Android - wanandroid.com 1、WanAndroid(鸿蒙版)开发的第一篇 其他一些参考点&#xff0c;请参考上面的WanAndroid开发第一篇 效果 首页实现 整体布局分为头部的Banne…

基于Gui Guider进行LVGL的页面绘制和移植

在之前的文章里讲过一种页面切换的方式&#xff0c;那就是&#xff1a;定义和创建页面对象绘制页面内容切换页面。参考这篇文章&#xff1a; LVGL如何创建页面并实现页面的切换-CSDN博客 这篇文章讲了如何绘制并切换页面。 但是现在遇到一个问题&#xff0c;那就是页面绘制&…

代码学习记录17

随想录日记part17 t i m e &#xff1a; time&#xff1a; time&#xff1a; 2024.03.12 主要内容&#xff1a;今天的主要内容是二叉树的第六部分&#xff0c;主要涉及二叉搜索树的最小绝对差 &#xff1b;二叉搜索树中的众数&#xff1b;二叉树的最近公共祖先。 530.二叉搜索树…

openGauss使用BenchmarkSQL进行性能测试(上)

一、前言 本文提供openGauss使用BenchmarkSQL进行性能测试的方法和测试数据报告。 BenchmarkSQL&#xff0c;一个JDBC基准测试工具&#xff0c;内嵌了TPC-C测试脚本&#xff0c;支持很多数据库&#xff0c;如PostgreSQL、Oracle和Mysql等。 TPC-C是专门针对联机交易处理系统…

【QT】文件流操作(QTextStream/QDataStream)

文本流/数据流&#xff08;二级制格式&#xff09; 文本流 &#xff08;依赖平台&#xff0c;不同平台可能乱码&#xff09;涉及文件编码 #include <QTextStream>操作的都是基础数据类型&#xff1a;int float string //Image Qpoint QRect就不可以操作 需要下面的 …

【Python】新手入门学习:详细介绍接口分隔原则(ISP)及其作用、代码示例

【Python】新手入门学习&#xff1a;详细介绍接口分隔原则&#xff08;ISP&#xff09;及其作用、代码示例 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、Py…

基于Qt 和python 的自动升级功能

需求&#xff1a; 公司内部的一个客户端工具&#xff0c;想加上一个自动升级功能。 服务端&#xff1a; 1&#xff0c;服务端使用python3.7 &#xff0c;搭配 fastapi 和uvicorn 写一个简单的服务&#xff0c;开出一个get接口&#xff0c;用于客户端读取安装包的版本&#…

<机器学习初识>——《机器学习》

目录 一、人工智能概述 1 人工智能应用场景 2 人工智能发展必备三要素 3 人工智能、机器学习和深度学习 二、人工智能发展历程 1 人工智能的起源 1.1 图灵测试 1.2 达特茅斯会议 2 发展历程 三、 人工智能主要分支 1 主要分支介绍 1.1 分支一&#xff1a;计算机视觉…

c++中string的模拟实现(超详细!!!)

1.string的成员变量、&#xff08;拷贝&#xff09;构造、析构函数 1.1.成员变量 private:char* _str;size_t _size; //string中有效字符个数size_t _capacity; //string中能存储有效字符个数的大小 1.2&#xff08;拷贝&#xff09;构造函数 //构造函数string(const char* …

【Linux进阶之路】HTTP协议

文章目录 一、基本概念1.HTTP2.域名3.默认端口号4.URL 二、请求与响应1.抓包工具2.基本框架3.简易实现3.1 HttpServer3.2 HttpRequest3.2.1 version13.2.2 version23.2.3 version3 总结尾序 一、基本概念 常见的应用层协议&#xff1a; HTTPS (HyperText Transfer Protocol Sec…

C# 8.0+版本项目 string不可为空

1.在某一次新建项目的时候发现&#xff0c;新建的项目&#xff0c;写的测试接口&#xff0c;接口的入参有string的参数&#xff0c; 但是调用接口的时候string的参数没有传报了400&#xff0c;很奇怪&#xff0c;也没有语法错误之类的。 2.解决办法 在项目上右键->属性->…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Span)

作为Text组件的子组件&#xff0c;用于显示行内文本的组件。 说明&#xff1a; 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 该组件从API Version 10开始支持继承父组件Text的属性&#xff0c;即如果子组件未设置…

直播美颜SDK的商业化应用:如何为直播平台带来更多商业机会?

直播过程中的自然环境和摄像头本身的限制可能会影响用户的体验&#xff0c;因此直播美颜SDK的商业化应用应运而生&#xff0c;它为直播平台带来了更多商业机会。 直播美颜SDK是一种集成在直播平台中的软件开发工具包&#xff0c;它能够对直播过程中的视频流进行实时的美颜处理…