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

一、背景

当谈到移动应用程序的体验时,页面启动速度是其中至关重要的一点,更快的页面展示速度确保应用程序可以迅速加载并响应用户的操作, 从而提高用户使用 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 并不能达成我们预期的结果。于是整个项目回归到了更高层级的目标上 —— 优化布局构建耗时。为了进一步优化布局构建的耗时,预加载、多线程构建,可谓“无所不用其极”,最后达成预期结果。

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

*文/令古

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

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

相关推荐

压测AI Agent 治理与评测调优实践|得物技术

本文介绍智能分析Agent在压测调试中上线后的治理闭环建设:通过Langfuse记录完整执行链路,结合用户反馈与Trace还原问题根因;利用CLI Mock固化证据数据集,实现相同场景下的回测验证。基于固定案例与五档评分机制,持续评估方案质量、延迟与成本,确保修复有效且无新问题引入,推动Agent能力迭代优化。

SmartCodeTech的博客 309

从中心调度到自驱协作多 Agent 架构的三次反转|得物技术

本文探讨多Agent系统中的控制权分配问题,提出三大协作架构:Master Agent(中心调度)、任务看板(共享状态)与Advisor(轻量咨询)。通过划清上下文边界与模型边界,强调应以信息相关性而非职业标签划分Agent。三种模式分别将决策权集中于中心、下放至共享状态、再下沉至执行者,形成控制权逐步下沉的演进路径。案例显示,合理设计可提升复杂任务的并行性、弹性与可维护性。

SmartCodeTech的博客 420

Jev 实战:为AI工作流引入轻量决策层 x10倍提速|得物技术

完成这三个场景后,我对适用边界有了更明确的认识:它们都可以先定义答案范围,再根据判断结果驱动下一步操作。接入时,我会优先明确输入、选项、采纳条件和兜底路径,再评估Jev是否适合承担这一环节。结合这些实践,我更容易理解Jev(TypeSafe System One)作为专用结构化决策模型不生成自然语言,而是返回带概率的判断结果。后续接入时,我会继续从脚本中边界明确的判断任务开始,分别验证准确性、耗时和费用,再决定是否扩大使用范围。下面整理了相关能力、限制和接入要点,便于查阅。

SmartCodeTech的博客 345

国产算力·昇腾 910C 大模型推理优化(下):源码修改与算子优化,提速30%|得物技术

本文延续上篇,深入vllm-ascend源码优化:通过多流重叠实现MoE共享专家并行计算,消除冗余事件开销,并解除与FUSED_MC2的互斥限制,实现c=1下TPOT从8.2ms降至8.0ms;优化decode阶段host侧开销,引入状态跟踪跳过冗余拷贝;设计Block Table CPU快路径,规避小batch下NPU kernel启动延迟;针对GDN层融合L2Norm至recurrent kernel,减少算子调用与内存往返。综合优化后推理性能显著提升,形成可复用的昇腾910C大模型推理优化方法论。

SmartCodeTech的博客 244

国产算力·昇腾 910C 大模型推理优化(上):镜像更新与参数调优,提速 40%|得物技术

文中详细给出每一项优化的动机、实现(含具体源码改动路径与关键代码片段)、验证数据与踩坑记录(包括十余项实测无效或负向的尝试),希望为同样在国产算力上做推理优化的团队提供一份可直接复用的工程参考。

SmartCodeTech的博客 225

动态规则的 Agent 实践:用确定性工程驾驭 LLM 的资产智能验收

面对财务验收中规则多变、材料非结构化、核对复杂等痛点,传统规则引擎难以应对。系统采用“规则即配置”思路,由大语言模型(LLM)负责理解合同、扫描件等非结构化材料并执行语义比对,工程系统则保障流程编排与可追溯性。通过异步处理、幂等设计、三类流量隔离及双通道(文本/图像)处理,实现高效稳定运行。结合用户反馈构建自动标准答案,辅以试跑、回测与发布门禁机制,形成闭环质量体系。底层采用确定性工作流架构,确保行为可预期、可测试,真正实现灵活规则的快速迭代与安全上线。

SmartCodeTech的博客 344

发一张图,它居然“跃“出屏幕了?一文读懂得物 3D 空间图片技术实践

3D 空间图片功能的落地,涉及模型生成、内容分发、客户端渲染之间的一系列取舍。本文重点分享如何生产出高质量、小体积的模型,以及如何在手机上流畅运行。

SmartCodeTech的博客 555

别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?

你搜"af1",它得给你"空军一号"。这一年,得物交易搜索把召回从"做选择题"改成了"写答案"——不再从商品池里"找"最像的,而是让模型自己"生成"你真正想要的。从 LLM 语义索引到 HLLM、语义 ID,搜索是怎么从"检索"走向"生成"的?

SmartCodeTech的博客 594

指标平台:从语义底座到智能消费的实践路径|得物技术

这篇文章想做的,不是在本体化的宏大叙事上再叠加一层理论,而是把我们在真实落地中验证过的路径讲清楚:语义底座怎么从零建立起来、怎么保证质量不失控、怎么支撑 Text2SQL 和 AI Coding 两条消费主线,以及沿着这条路径往前走,下一步该往哪个方向发力。

SmartCodeTech的博客 316

企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术

基于 AgentScope Java 的企业级 MultiAgent 平台,为研发、数据分析和知识问答等场景提供统一的模型调用、工具接入、任务编排、流式输出与运行追踪能力。

SmartCodeTech的博客 302

得物小摊 AI Native 演进实录:用 Harness 构建可控 AI 交付

AI Coding 一个人用很顺手,进了团队就漏:指令没人审、经验难沉淀、过程看不见、链路不闭环。作者团队基于 Multica 做了 EP-Harness,把 Agent 当成员管,给它套一层可执行、可观察、可治理的 Harness,并让反馈闭环。成果:可追溯决策、多 Agent 协作,高频异常从每 4 小时 2400 多条降到个位数,对得物技术补上团队级 AI 协作关键一环。

SmartCodeTech的博客 542

企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术

在 MultiAgent 平台中,Agent 的"记忆"能力是实现智能对话和个性化服务的核心资产。本文深入剖析 Agent 记忆系统的设计理念与实现细节,重点解析长期记忆架构的写入与加载流程,涵盖 LLM 智能判断、语义检索、Token 预算控制、冲突处理等关键技术,为开发者提供完整的技术实现参考。

SmartCodeTech的博客 502

EP-Harness:从个人 AI Coding 到团队级 Agent 工作流|得物技术

AI Coding 个人用得爽,一到团队就崩。EP-Harness 把 Agent 从聊天工具变成团队协作成员——可管理、可追溯、可闭环。落地后高频异常从 2400+/4h 收敛到个位数。

SmartCodeTech的博客 694

得物知识问答:复合检索 Agent 的系统设计实践

从"搜到什么用什么"到"搜→筛→挖→验"自主决策闭环——我们造了一个有判断力的 RAG Agent,不是套壳。

SmartCodeTech的博客 734

实战从零开始构建一个Coding Agent:Violin |得物技术

理解了 coding agent 的构建原理,也就掌握了理解其他 agent 的一把钥匙。笔者对每天都在使用的 agent 原理很感兴趣,于是决定自己写一个:violin。

SmartCodeTech的博客 927

AI Native 交易核心系统的研发范式|得物技术

如何改造旧研发流程适配 AI 编码?本文提出五道标准化关口治理思路,依托 Claude Code Spec-Driven 插件打通需求澄清、方案设计、TDD 落地、门禁管控与埋点监控,构建带人工确认门控的完整工作流,以五层自底向上架构实现 AI 编码规范化落地。

SmartCodeTech的博客 975

推荐系统体验的数字化突破:得物自动化评测平台的技术实践|AICon 文章整理

「得物推荐 AI Harness 工程化实践系列」的末篇内容,本系列共三篇连载。本篇(末篇)详解得物推荐评测平台的完整技术体系:从主观体验量化到大模型自动化评测流水线,涵盖多维度指标设计、人机协作提示词校验闭环、分布式任务调度与成本精控等核心工程实现,以及单周百万级审计需求下实现 91% 资源成本优化的工业级落地实战细节。

SmartCodeTech的博客 1493

RAG 核心概念与原理:Chunking、Embedding、相似度、HNSW 与多路召回|得物技术

决定 RAG 效果的关键不在大模型,而是文本切块、查询改写、多路召回与精排。合理切块存好知识库内容,向量匹配抓语义、BM25 匹配关键词互补。先优化用户提问,快速召回一批相关文档,再用精排模型精细筛选,整套检索流程配合到位,问答准确度才会明显提升。

SmartCodeTech的博客 1079

从“机械应答“到“服务伙伴“:得物高可控智能客服的 Agent 工程实践|AICon 演讲整理

本次分享围绕以下核心技术展开:客服 Agent 挑战与架构设计(Single-Agent→Multi-Agent→Harness);高可控性数据飞轮(PE 自动化 + DPO 训练);RL 策略训练和人工经验对齐;多轮半双工消息流控制。

SmartCodeTech的博客 749

得物推荐系统诊断 Agent:从 “调接口” 到 “会思考”|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。AICon上海2026_全球人工智能开发与应用大会暨大模型应用生态展_InfoQ技术大会「得物推荐 AI Harness 工程化实践系列」的中篇内容,本系列共三篇连载。本篇(中篇)进一步详解得物推荐复杂业务场景下,AI代码从生成、防护校验到安全上线的完整技术体系、自研AI Harness整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。

SmartCodeTech的博客 1364

得物 OceanBase 落地实践

得物选型OceanBase多模数据库应对业务挑战,通过验证其高可用架构、存储压缩技术及混合负载处理能力。测试显示OceanBase在复杂查询场景性能较MySQL提升130-200倍,在高并发TP/AP混合场景表现优异,存储成本降低40%+。通过DBA内部报表业务试点验证其稳定性后,逐步推广至核心业务场景,优化数据库技术栈,实现性能与成本的双重收益。

SmartCodeTech的博客 1382

AI UITester:AI Native 的 UI 自动化测试新范式|得物技术

ai_uitester 代表了 AI Native 的 UI 自动化测试新范式:这不仅是工具的升级,而是测试范式的转变 ------ 从 "代码驱动" 转向 "视觉驱动",从 "人工调试" 转向 "AI 自愈",从 "三端分离" 转向 "统一抽象"。

SmartCodeTech的博客 779

从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。「得物推荐 AI Harness 工程化实践系列」的开篇内容,本系列共三篇连载。将系统拆解得物推荐复杂业务场景下,AI 代码从生成、防护校验到安全上线的完整技术体系,逐一详解自研 AI Harness 整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。本篇(上篇):将整体介绍团队自研的 AI Harness 体系,包括建设思路、全生命周期防护机制、混合智能体架构及落地效果。

SmartCodeTech的博客 1080

从表单到 Agent:得物社区活动搭建的 AI 实践之路

一场营销活动从策划到上线,运营要在三个系统间跳转 10 + 次、填写 40 + 个字段。我们用 AI 重新设计了这条链路 —— 从 “AI 帮你填表单” 到 “两阶段 Agent + 聚合工作台”。这篇文章记录的不是技术细节,而是这条路上的选择和反思。

SmartCodeTech的博客 744

从埋点需求到规则资产:Hermes Agent 重构得物数仓工作流

埋点和指标需求中,消耗数据承接方的不是生成答案,而是补齐证据链、对齐历史口径、识别下游影响,并在生产写入前留下可追溯的确认记录。本文数据承接方指承接业务需求的数据同学或业务合作伙伴;后文讨论内部研发、元数据、查询和门禁能力在流程中的角色。

SmartCodeTech的博客 656

让 Claude Code 拥有自我进化和记忆系统|得物技术

本文记录了为 Claude Code 构建的一套持久化记忆与自我学习系统的设计思路与实现细节。这套系统让 AI 助手能够跨越会话边界记住用户习惯,并在每次对话结束后自动提炼行为规律和系统信息,通过向量检索快速提取关键信息,下一次对话时主动应用,逐步进化为更懂你的编程伙伴。

SmartCodeTech的博客 994

用 LLM Agent 重构告警排查流程|得物技术

告警排查需频繁切换日志、APM、链路追踪等多个平台,耗时 10~30 分钟,且高度依赖个人经验。我们做了 Troubleshooter——用 LLM Agent 自动完成数据采集、根因分析和处置建议生成。上线后中位数排查耗时从 20 分钟降到 4.4 分钟,覆盖 11 个服务和 10+ 种告警类型。

SmartCodeTech的博客 1007

HorizonVault 技术深潜:如何在 HDD 上做出 100GB/s+ 级大吞吐分布式存储|得物技术

HorizonVault是一款面向Kafka远程存储的高吞吐分布式存储引擎,通过分布式并行写入和状态感知调度解决冷数据存储的三大核心问题:吞吐瓶颈、性能抖动和恢复能力。系统采用分层架构,Meta负责全局资源调度,Broker处理请求路由,Store实现本地磁盘管理,形成完整的写入治理链路。通过将HDD磁盘提升为一等资源,实现顺序追加写入、多盘并行和慢盘隔离,在保证高吞吐的同时控制延迟。存储结构采用DiskStore组织磁盘资源,LogSegment顺序写入数据,IndexSegment定位读取,Change

SmartCodeTech的博客 892

Claude Code Harness 工程:数仓侧落地方案|得物技术

Harness 工程通过职责分层实现研发流水线可靠性提升。Claude 负责需求理解与代码生成等语义层工作;hooks 负责规范检查等确定性执行;Subagents 隔离血缘查询、自测等高 token 操作;Memory 承载跨会话状态持久化。四层协同使数仓 AI 开发从"对话式辅助"转变为"规则驱动的自动化流程"。

SmartCodeTech的博客 1101

BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

本文为 FlinkSpec 系列之开篇,亦是这场工程化变革的序章。BP Claw 所立之处,不过链路之源。FlinkSpec 所图,乃以 AI 之力,将实时数仓从需求落地至验收上线的全程工序,熔铸为一套精密自洽、生生不息的智能工程体系。宏图未竟,后续系列将逐章揭幕,敬请期待!

SmartCodeTech的博客 1038
上一篇: 商家可视化埋点探索和实践
下一篇: 如何扩展大模型的上下文长度
得物技术
得物技术 得物技术官方账号 得物技术官方账号
博客等级 码龄6年 2492粉丝 408原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值