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

news2024/10/5 13:32:40

一、背景

当谈到移动应用程序的体验时,页面启动速度是其中至关重要的一点,更快的页面展示速度确保应用程序可以迅速加载并响应用户的操作, 从而提高用户使用 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">

  <include
      android:id="@+id/head"
      layout="@layout/head"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerHorizontal="true" />

  <ImageView
      android:id="@+id/ccc"
      style="@style/bb"
      android:layout_below="@id/head" />
</RelativeLayout>

转换成 Java 文件:

public class X2C_2131296281_Activity_Main implements IViewCreator {
  @Override
  public 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() {
                @Override
                public 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">

    <TextView
        android:id="@+id/tv_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="测试一下效果"
        android:textSize="24sp" />

    <com.shizhuang.x2c.CustomView
        android: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 {
  @Override
  public 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;
  }

  @Override
  public 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);
  }

  @Override
  protected 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: Context
    override 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.coloradmin.cn/o/1512859.html

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

相关文章

jmeter发送请求参数如何使用变量

问题描述 发送jmeter请求时&#xff0c;想设置请求参数为变量 解决方法

190基于matlab的tfrSTFT时频分布图

基于matlab的tfrSTFT时频分布图&#xff0c;计算时间序列的STFT时频分布图&#xff0c;得到瞬时频率。通过GUI可以调节图像的展示样式。程序已调通&#xff0c;可直接运行。 190 STFT时频分布图 瞬时频率 能量谱 (xiaohongshu.com)

剪切板工具Ditto

https://github.com/sabrogden/Ditto/releases 1.开源免费Ditto 快捷键 ctrl (j键盘 esc下面的符号) 挺好用的&#xff0c;解决问题 投标中需要重复填写的内容&#xff0c;可以通过他进行 实时复制信息 2.windows自带的黏贴板工具 win键v调出快捷键

数字孪生+工业互联网标识解析,打造智能工厂新标杆!

当前&#xff0c;工业4.0浪潮愈发澎湃&#xff0c;加快数字化、网络化、智能化发展成为了制造业转型升级的必然要求。 51WORLD基于数字孪生技术与工业互联网标识解析体系&#xff0c;打造了一个集协同化供应、个性化定制、智能化生产于一体的全连接产线孪生平台&#xff08;以…

电脑记事本分类密码怎么设置?记事本备忘录分类密码设置方法

身为一名文字工作者&#xff0c;我每天都需要在电脑上记录大量的信息和灵感。电脑记事本备忘录对我来说&#xff0c;就像是一位随时待命的助手&#xff0c;帮助我捕捉每一个稍纵即逝的想法。然而&#xff0c;在开放的办公环境中&#xff0c;我总有些隐隐的担忧——毕竟&#xf…

【Python】新手入门学习:详细介绍组合/聚合复用原则(CARP)及其作用、代码示例

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

Affinity Photo:像素大师,影像重塑者 mac/win版

在数字图像处理领域&#xff0c;Affinity Photo已经崭露头角&#xff0c;成为许多专业摄影师和图像设计师的首 选工具。这款软件不仅具备丰富的功能和强大的性能&#xff0c;还提供了直观易用的操作界面&#xff0c;让用户能够轻松实现高质量的图像处理。 Affinity Photo 软件…

通过el-select选择器和el-tree树形结构二次封装(vue2+elementUI),开发element-plus的TreeSelect树形选择器

需求&#xff1a; 领导看我在另一个vue3项目的el-tree-select挺好的&#xff0c;叫我移入vue2的项目中。 但是vue2版本的elementUI&#xff0c;并没有这个组件&#xff0c;所以只能自己找&#xff0c;找半天找不到和它差不多的&#xff0c;通过网友写的组件改写的 参考链接&…

vMware WorkStation创建虚拟机安装CentOS7,NAT模式配置网络

一、安装虚拟机 1、选择典型&#xff08;推荐&#xff09;配置 2、选择稍后安装操作系统 3、操作系统选择CentOS7 64位 4、虚拟机命名、选择安装位置 6、指定磁盘容量 7、指定磁盘容量 步骤&#xff1a; &#xff08;1&#xff09;、系统内存2GB &#xff08;2&#xff09;、…

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* …

IDEA 2022.1以上版本 配置使用新UI

1、进入此页面的快捷建CtrlAltShift/ 2、新UI配置 3、原设置

算法练习:二分查找

目录 1. 朴素二分查找2. 在排序数组中查找元素的第一个和最后一个位置3. 搜索插入位置4. x的平方根5. 山脉数组的峰值索引6. 寻找峰值7. 寻找旋转排序数组中的最小值8. 点名 1. 朴素二分查找 题目信息&#xff1a; 题目链接&#xff1a; 二分查找二分查找的使用前提为数据具有&…

System类 --java学习笔记

System System代表程序所在的系统&#xff0c;也是一个工具类 常见System方法&#xff1a; 按照惯例&#xff0c;exit括号中非零状态码表示异常终止&#xff0c;填零则表示人为终止 currentTimeMillis&#xff08;&#xff09;返回的是long类型的时间毫秒值&#xff1a;指的…

iOS增量报告生成方案

一&#xff0c;iOS覆盖率报告生成逻辑 iOS覆盖率报告生成与Android有很大的不同&#xff0c;主要的生成逻辑如下&#xff1a; 1&#xff0c;将profraw文件&#xff0c;通过命令xcrun llvm-profdata merge -sparse转换成profdata; 2&#xff0c;再将profdata文件&#xff0c;通…

SCI 机器视觉领域期刊,审稿周期

(1)pattern recognition letters, 从投稿到发表&#xff0c;一年半时间 (2)Pattern recognition 不好中&#xff0c;时间长 (3)IEICE Transactions on Information and Systems&#xff0c; 作者中有一个必须是会员。收费高&#xff0c;审稿快。影响因子0.4 (4)Internationa…

SpringBoot实战项目——博客笔记项目

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、项目介绍二、项目的整体框架 2.1 数据库模块 2.2 前端模块 2.3 后端模块三、项目图片展示四、项目的实现 4.1 准备工作 4.…

PHP爬虫技术:利用simple_html_dom库分析汽车之家电动车参数

摘要/导言 本文旨在介绍如何利用PHP中的simple_html_dom库结合爬虫代理IP技术来高效采集和分析汽车之家网站的电动车参数。通过实际示例和详细说明&#xff0c;读者将了解如何实现数据分析和爬虫技术的结合应用&#xff0c;从而更好地理解和应用相关技术。 背景/引言 随着电…

git svn混用

背景 项目代码管理初始使用的svn, 由于svn代码操作&#xff0c;无法在本地暂存&#xff0c;有诸多不便&#xff0c;另外本人习惯使用git. 所以决定迁移至git管理 迁移要求&#xff1a; 保留历史提交记录 迁移流程 代码检出 git svn svn_project_url git代码提交 修改本…

突然估摸出了chrome数据的备份

首先是下载 其默认下载到c盘。 我们打开刚刚安装的chrome的位置&#xff0c; 我电脑上是 C:\Users\Lenovo\AppData\Local\Google\Chrome\Application 第一个文件名和版本号对应。 我们查看其上级目录&#xff0c;可以发现有个User Data&#xff0c;这个文件夹里面存放的就是…

发送短信验证码

​​​​​​【短信验证码-快速报备签名】三网短信接口-短信-短信验证码-短信服务-三网短信接口-短信-三网短信【最新版】_商业智能_电商_金融-云市场-阿里云阿里云云市场提供 专注企业短信服务10年运营与技术积累&#xff0c;稳定、安全、快速。服务&#xff0c;建站服务&…