Android页面渲染效率优化实践

Android页面渲染效率优化实践

 

1.车系页布局渲染现状 

车系页是重要的车系信息页面,更新迭代多年,页面布局不断变化,xml布局文件越写越复杂。

获取车系页布局文件耗时:

        startTime = System.currentTimeMillis();
        setContentView(R.layout.car_series_revision_activity);
        long durTime = System.currentTimeMillis() - startTime;
        LogHelper.e("布局总耗时","车系页布局耗时:" + durTime);

结果如下:

图片

 

2.卡顿的原因

2.1

Android绘制原理

► 1.Android的屏幕刷新中涉及到最重要的三个概念

(1)CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU

(2)GPU:进一步处理数据,并将数据缓存起来

(3)屏幕:由一个个像素点组成,以固定的频率(16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点

总结一句话就是:CPU 绘制后提交数据、GPU 进一步处理和缓存数据、最后屏幕从缓冲区中读取数据并显示。

图片

► 2.双缓冲机制

图片

当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理。

当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。

如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换。

这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的掉帧。

2.2

布局加载原理

页面启动时,布局加载在主线程上进行耗时操作,会导致页面渲染及加载慢。

布局加载主要通过setContentView来实现,下面是它的调用时序图:

图片

我们可以看到,在setContentView中主要有两个耗时操作:

(1)解析xml,获取XmlResourceParser,这是IO过程。

(2)通过createViewFromTag,创建View对象,用到了反射。

以上两点就是布局加载慢的原因,也是布局的性能瓶颈。

 

3.布局加载优化

上一章分析了布局加载慢的主要原因,因此,我们的优化方式主要有以下两种:

(1)异步加载,将布局加载过程转移到子线程

(2)去掉IO和反射过程

3.1

异步加载,AsyncLayoutInflater方案

 setContentView 默认是在UI主线程加载布局的,其加载过程中的耗时操作,如解析xml,反射创建view对象等也是在主线程执行,AsyncLayoutInflater 可以让这些加载过程在子线程中执行,这样可以提高UI线程的响应性,UI线程同时可以进行其他操作。AsyncLayoutInflater使用方式如下:

new AsyncLayoutInflater(this).inflate(R.layout.car_series_revision_activity, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
                setContentView(view);
            }
        });

AsyncLayoutInflater方案的缺点:

(1) UI布局和view的初始化在子线程中进行,如果view还未初始化成功,在主线程中再调用view会引起崩溃。

(2) 一般情况下,主线程会调用view,涉及到大量子线程和主线程在view调用上的同步问题,这就牺牲了易用性,代码可维护性也会变差。

(3) 如果是在老页面逻辑结构上引入AsyncLayoutInflater进行改造,结构改动很大,很容易发生view调用崩溃错误,不太可行。

3.2

X2C方案

 X2C 是掌阅开源的一套布局加载框架。X2C的主要思路是利用apt工具,在编译期将我们写的xml布局文件解析成view,并根据xml动态设置view的各类属性,这样,我们在运行时,调用findViewById,根据view id拿到的view,已经是直接new 出来的view,避免了运行时的xml IO操作和反射操作,这就解决了布局时的耗时问题。

原始的xml布局文件:

<LinearLayout 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:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <Button
        android:id="@+id/x2c"
        style="@style/btn"
        android:text="X2C" />
    <Button
        android:id="@+id/xml"
        style="@style/btn"
        android:layout_marginTop="10dp"
        android:text="XML" />
    <Button
        android:id="@+id/sub"
        style="@style/btn"
        android:layout_marginTop="10dp"
        android:text="subModule" />
</LinearLayout>
X2C 编译期apt生成的java文件:
public class X2C127_Activity implements IViewCreator {
  @Override
  public View createView(Context ctx) {
     Resources res = ctx.getResources();
        LinearLayout linearLayout0 = new LinearLayout(ctx);
        linearLayout0.setTag(R.id.x2c_rootview_width,ViewGroup.LayoutParams.MATCH_PARENT);
        linearLayout0.setTag(R.id.x2c_rootview_height,ViewGroup.LayoutParams.MATCH_PARENT);
        linearLayout0.setId(R.id.constraintLayout);
        linearLayout0.setGravity(Gravity.CENTER);
        linearLayout0.setOrientation(LinearLayout.VERTICAL);
        Button button1 = new Button(ctx);
        LinearLayout.LayoutParams layoutParam1 = new LinearLayout.LayoutParams((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,150,res.getDisplayMetrics())),(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,50,res.getDisplayMetrics())));
        button1.setBackgroundColor(res.getColor(R.color.colorAccent));
        button1.setTextSize(TypedValue.COMPLEX_UNIT_DIP,20);
        button1.setGravity(Gravity.CENTER);
        button1.setTextColor(Color.parseColor("#ffffff"));
        button1.setId(R.id.x2c);
        button1.setText("X2C");
        button1.setLayoutParams(layoutParam1);
        linearLayout0.addView(button1);
        return linearLayout0;
  }
}

X2c的优点:

(1)易用性和可维护性好,对原有代码侵入性不强,应用代码还是使用xml写布局

(2)加载耗时可缩短到原来的1/2到1/3

X2c的缺点:

(1)View的属性支持不完全

(2)兼容性和稳定性不是很高,在高版本的gradle 编译工具,如gradle3.1.4,会出现找不到R.java文件,找不到xml对应的java文件等问题

(3)目前,X2C更新到2021年,并没有持续维护和解决issue

3.3

Compose方案

Compose 是 Jetpack 中的一个新成员,是 Android 团队在2019年I/O大会上公布的新的UI库。

Compose使用纯kotlin开发,使用简洁方便,但它是完全抛弃了View 和 ViewGroup这套系统,自己把整个的渲染机制从里到外做了个全新的,是未来取代XML的官方方案。

Compose的优点:

(1)使用声明式UI,摒弃了xml布局运行时解析,布局效率更高

(2)使用kotlin开发,简单易用,布局形式上跟flutter统一。

如果是使用kotlin开发的新项目,可以引入Compose方案,对于老项目的优化,Compose方案并不适用。

3.4

我们的优化方案-在布局反射上做文章

 Xml解析到view,完全自己来做,比较复杂且有很多风险,这个过程涉及到两个耗时的点:

(1)xml解析,IO操作

(2)反射

xml解析这部分工作复杂度很高,可以交给android系统来做。我们可以想办法去除反射的逻辑。

我们需要找到一个反射生成view的入口。我们知道,View生成相关逻辑在LayoutInflater的createViewFromTag中,调用了onCreateView(parent, name, context, attrs),通过反射生成了view。

通过android系统的LayoutInflater setFactory,我们不仅可以控制View的生成,还可以把View变成另外一个View。在setFactory的onCreateView(parent, name, context, attrs)回调中,我们接管单个view的生成,去掉反射,new 出我们自己的view就解决了问题。而onCreateView(parent, name, context, attrs)中的参数name返回的就是xml中使用到的view的名字,根据这个name,直接new出来新的view。方式如下:

        LayoutInflaterCompat.setFactory(getLayoutInflater(), new LayoutInflaterFactory() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                switch (name) {
                    case "TextView":
                        return new TextView(context, attrs);
                    case "ImageView":
                        return new ImageView(context, attrs);
                    case "com.cubic.choosecar.ui.car.view.NewStarView":
                        return new com.cubic.choosecar.ui.car.view.NewStarView(context, attrs);
                    case "com.cubic.choosecar.ui.carseries.scrolllayout.ScrollableLayout":
                        return new com.cubic.choosecar.ui.carseries.scrolllayout.ScrollableLayout(context, attrs);
                    case "View":
                        return new View(context, attrs);
                    case "com.cubic.choosecar.newui.carseries.view.CarRevisionSeriesImageScaleLayout": //自定义view
                        return new com.cubic.choosecar.newui.carseries.view.CarRevisionSeriesImageScaleLayout(context, attrs);
                    case "ViewStub":
                        return new ViewStub(context, attrs);
                    case "ScrollView":
                        return new ScrollView(context, attrs);
                    case "androidx.constraintlayout.widget.ConstraintLayout":
                        return new androidx.constraintlayout.widget.ConstraintLayout(context, attrs);
                    case "FrameLayout":
                        return new FrameLayout(context, attrs);
                    case "RelativeLayout":
                        return new RelativeLayout(context, attrs);
                    case "androidx.appcompat.widget.Toolbar":
                        return new androidx.appcompat.widget.Toolbar(context, attrs);
                    case "LinearLayout":
                        return new LinearLayout(context, attrs);
                    default:
                        View view = getDelegate().createView(parent, name, context, attrs);
                        return view;
                }
                //return view;
            }
        });

 

包括系统view和我们自定义的view。

此方案对已有项目的代码侵入性很小,改造成本低,兼容性也很高,相对来讲,在渲染效率上比X2C方案低一些,但比较匹配我们对已有旧项目复杂布局的渲染优化。

 

3.5

进一步在布局上优化

 我们可以使用viewStub实现布局的懒加载。思路是将布局分成不同的模块,让部分模块使用viewStub标签替代,一半屏幕的模块元素渲染完成以后,再通过viewStub来渲染生成viewStub所包含的其它模块,实现延迟渲染加载。

通过分析车系页布局,已经将布局元素,按功能做了一些模块的划分,我们进一步将关联度大的布局模块集中在一起,封装在一个自定义VIEW中,使用viewStub包含替换这些模块View。UI线程setContentView渲染布局时,viewStub所包含的模块并不会被渲染,只会渲染屏幕的部分元素,等待主接口数据返回,再使用viewStub延迟其它模块,实现了布局的懒加载,加快了主线程的渲染速度。

 

4.优化结果

通过3.4和3.5节的优化方法,车系页复杂布局渲染优化对比结果如下:

图片

通过对比可以看到,在不同档次的android机型上,渲染耗时降低了20%-35%左右,在低端机型上,减少的绝对耗时更多,感受可能会明显一些。

 

作者|蒋雄锋

原文链接:https://www.cnblogs.com/88223100/p/Optimization-Practice-for-Android-Page-Rendering-Efficiency.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Android页面渲染效率优化实践 - Python技术站

(0)
上一篇 2023年4月3日
下一篇 2023年4月17日

相关文章

  • RxJava 异常时堆栈显示不正确?解决方法都在这里

    本文首发我的博客,github 地址 大家好,我是徐公,今天为大家带来的是 RxJava 的一个血案,一行代码 return null 引发的。 前阵子,组内的同事反馈说 RxJava 在 debug 包 crash 了,捕获到的异常信息不全。(即我们捕获到的堆栈没有包含我们自己代码,都是一些系统或者 RxJava 框架的代码) 典型的一些 error 信息…

    Android 2023年4月18日
    00
  • 钥匙环服务,打造无缝的跨设备登录体验

    近些年来,随着手机技术迭代更新越来越快,用户更换手机的周期也在缩短,在这样的背景下,开发者不得不面临以下问题: 同一开发者旗下常常有多个安卓应用和多形态应用(快应用和Web应用),用户更换一个新的设备(手机或平板)后,在新设备上登录各应用时每次都需要重复输入帐号和密码,导致用户在登录阶段流失率增加,同时开发者还需要承担额外的短信成本(如用户使用短信登录)。 …

    Android 2023年4月17日
    00
  • 在线文本翻译能力新增14个直译模型,打造以中文为轴心语言的翻译系统

    经济全球化的今天,人们在工作和生活中经常会与外语打交道。相较传播性较广的英语而言,其他语种的识别和阅读对大多数人来说是一件难事,此时就需要借助语言翻译软件来帮助理解。 华为 HMS Core 机器学习服务(ML Kit)翻译功能提供了多种翻译模式,不仅可以满足应用出行购物、网络社交等日常场景,还提供办公文档、视频字幕等专业翻译服务,满足多种语言和场景应用。其…

    Android 2023年4月18日
    00
  • 跑步课程导入能力,助力科学训练

    HUAWEI Health Kit为开发者提供用户自定义的跑步课程导入接口,便于用户在华为运动健康App和华为智能穿戴设备上查看来自生态应用的训练课表,开启科学、适度的运动训练。 跑步课程导入能力支持生态应用在获取用户的华为帐号授权后,将跑步课程数据写入至华为运动健康App,并在已有的华为智能穿戴设备连接并支持课程导入时,直接将课表推送到设备上,用户可以轻松…

    Android 2023年5月11日
    00
  • 投放视频广告时,如何快速与第三方播放器兼容?

    新媒体时代,广告样式越来越丰富。相较于传统的图文信息,视频类广告更具有直观性,能够让消费者在了解产品知识和功能的同时加深对产品的印象。 因此在各类网站或App上投放视频类广告是个很好的宣传方式,但广告商们如果想在网站上展示视频广告,必须确保视频广告投放协议与发布渠道的播放器兼容;如果不能兼容,广告商或者视频广告投放平台必须创建不同的响应,来与特定视频播放器兼…

    Android 2023年4月17日
    00
  • 语言录制兼容长按跟点击录制

    录音需求中,往往有两种常规操作。 长按基本实现流程: 监听触摸事件,按下时录制,抬起时停止。 点击基本流程: 点击开始录制,在次点击停止录制 但是凡事有绝对,如果需要同时支持长按录制抬起结束跟点击录制在次点击结束呢?面对如此无理的需求,从技术层面上怎么如丝滑般去兼容呢。 需要两者兼容,只能从触摸事件入手了,这里的重点其实就在于怎么在触摸事件中去区分点击事件跟…

    Android 2023年4月22日
    00
  • Xcode 工程分析

    1. 背景 Xcode作为日常开发iOS程序的IDE,支持C、C++、Objective-C、Swift、Ruby等语言进行编写。日常开发入口就是Xcode workspace或者Xcode project。 workspace是一个Xcode文档,它将项目和其他文件、project分组。一个workspace可以包含任意数量的Xcode project,以…

    IOS 2023年4月17日
    00
  • .NET Core部署到linux(CentOS)最全解决方案,进阶篇(Supervisor+Nginx)

    在.NET Core部署到linux(CentOS)最全解决方案,常规篇一文,我们详细讲解了传统的.NET Core部署到Linux服务器的方法,学到了Linux在虚拟机下的安装、Xshell,Xftp的使用方法、git在linux下的交互使用以及.net core在linux下的发布与运行全过程。本文讲讲解通过使用Supervisor+Nginx的组合来实…

    C# 2023年5月5日
    00
合作推广
合作推广
分享本页
返回顶部