Android高性能编码 - 第四篇 layout加载UI优化

本篇对Android应用的UI渲染加载性能相关项进行阐述,这里的layout既包括使用xml文件定义的layout资源,也包括java代码动态创建的UI资源以及自定义组合资源,其中图片相关的部分已在第三篇进行集中阐述,这里不再细表,而是从一般意义上的UI界面加载层面进行探讨。

4.1 Activity基类布局和子类布局的协调

根据编码实践,我们通常会为Activity抽取基类,同时在基类中抽取父布局,抽象出接口给子类实现布局中的Content;或者直接使用Fragment,加入宿主的布局中。于是在具体的layout加载之前,我们首先遇到的一个问题就是,两者中的布局如何协调。

父子Activity嵌套是比较常用的场景,具体而言,在父类加载通常是一些背景和低层次的外围布局,但无论如何,我们都将面临一些具体的问题,包括:

1.        加载两次布局,执行一次子布局的嵌入

2.        两个布局叠加,层级过深

3.        父类布局加载了当前子布局并不必要的背景等低层元素

4.        如果使用Fragment的基类,形成BaseActivity - Activity - BaseFragment - Fragment的布局关系,嵌套问题可能更显著。

无疑上述问题会带来相应的性能问题,考虑从以下方面进行规范。

4.1.1 不在基类Activity加载布局

在使用基类Activity的界面,原则上可以不在父类布局,整体layout抽象给子类实现,减少一次布局的加载,使GPU的渲染任务更为集中地处理;

对于需要抽取的低层次布局元素,比如原来放在父类Activity的通用的静态背景元素可以独立为xml文件,在子类中利用xml的include等方式复用即可。

基类Fragment同理。

4.1.2 严格控制Activity低层级布局层次

即使无法避免使用父级布局,如使用Activity + Fragment方式,因为此时Activity里面的元素一般都是低层级并且match_parent性质,对后续上层级布局有全局的影响。

为保持整体的扁平化,并留充足的空间给上层实现细节,要求不超过2层标签。

比如Activity提供一层background整体容器,再提供一层Fragment的容器,比如Viewpager。

4.1.3 检查非必须的元素

一般包括需要全屏显示的加载失败等提示,处理方式有两种:

1.        使用ViewStub在确切需要时再加载;

2.  使用代码手动生成添加。两种方式都是为了避免将option性质的控件占用布局初始化加载时间和空间。

4.2 扁平化布局

接下来阐述布局的扁平化要求。由于Android的layout加载特性,每个控件的渲染,都会引起其所在父容器的所有元素的一次集体测绘渲染,因此,Android中应该极力避免层级较深的设计,否则容易导致过度绘制的问题。

从编码设计的角度来说,扁平化的规范至少要考虑以下方面。

备注:对于过度绘制的要求和检测,请参考性能诊断文档,在此不作细表。

4.2.1 消除不必要的嵌套

4.2.1.1 消除多余的嵌套

多余的嵌套也需并非开发者本意,而是在多次界面扩张堆叠需求的结果,而这样的问题,使用HierarchyViewer等相关工具甚至review代码的方式就可以轻松发现的,因此应该得到及时的重构。

4.2.1.2 复杂局部灵活使用RelativeLayout

在一些局部的复杂区域,往往是容易发生过深层级的地方,此时应该充分使用相对布局,而避免线性布局的过度嵌套。

4.2.2 消除不必要的背景元素

背景元素是非常容易忽略的一个层级要素,包括:

应用的Window层级和Activity底层级一般都默认有相关主题背景;

ImageView等控件设置了不必要的背景;

因此,规范要求,至少从以下几个方面检查背景元素的重复问题:

1.        检查界面组件间的背景重复性

各层级的主题、Activity、Fragment,对背景元素进行统一安排,为保持弹性,一般建议将主题中的背景主动置空,或者按照具体Activity的情况,制作多个主题,在需要统一background的Activity给主题配置背景元素。

2.        检查layout中容器上下级的重复背景

尤其是match_parent的低层级元素,由于被整体覆盖,可能没有及时发现其重复设置的背景。

3.        检查控件的多余背景

常见的包括:

l  在布局文件中,ImageView中不必要地放入了图片甚至selector。图片控件一般有代码层面调用图片加载接口时,设置其默认的加载图片,因此在xml文件中,不必填充图片资源,避免在初始化时drawable加载;

l  在textView中不必要的设置了颜色等属性

Textview中的字体颜色如果是动态设置,则不要事先在xml文件中指定,避免初始化时不必要的颜色资源加载。

 

一个优化示例:

优化之前:3大块,每块4层;大量嵌套;组合外围全是LinearLayout;Image的背景图片不必要,代码里面动态设置等等;

优化过后:

只有一个大块,大块最深3层,另外两个大块释放为一层;全部使用RelativeLayout等等

4.3 应用三大标签减少层级

扁平化的布局,一方面要求我们从视图树的设计上进行斟酌,另一方面,为了对layout元素进行解耦和复用,并且在复用过程中减少一个多余的层级,SDK提供了三个有用的标签供开发使用。

4.3.1 <ViewStub/>标签

ViewStub是一个不可见的,大小为0的View,最佳用途就是实现View的延迟加载,在需要的时候再加载View,即实现延迟加载。

当调用ViewStub的setVisibility函数设置为可见或则调用inflate()方法初始化该View的时候,ViewStub引用的资源开始初始化,然后引用的资源会替代掉ViewStub,把自己填充在ViewStub的原位置。因此在没有调用setVisibility(int)或inflate()方法之前ViewStub会一直存在组件树层级结构中,但是由于ViewStub非常轻量级,这对性能影响非常小。

可以通过ViewStub的inflatedId属性来重新定义引用的layout的id(也就是说,加入我们所引用的layout的id是R.layout.prelayout,然后我们在ViewStub中添加属性android:inflatedId="@+id/subLayout“,那么我们就可以用R.layout.subLayout来引用这个layout了)。

示例:

上面定义的ViewStub我们可以通过findViewById(R.id.stub)来找到,在初始化资源preLayout后,ViewStub从父组件中删除,然后preLayout替代ViewStub的位置。初始资源preLayout得到的组件可以通过inflatedId指定的id "subLayout"引用。 然后初始化后的资源被填充到一个120dip宽、40dip高的地方。

推荐使用下面的方式来初始化ViewStub:

ViewStub stub = (ViewStub)findViewById(R.id.stub);

View inflated = stub.inflate();

当调用inflate()函数的时候,ViewStub被引用的资源替代,并且返回引用的view。这样程序可以直接得到引用的view而不用再次调用函数findViewById()来查找了。

备注:ViewStub目前有个缺陷就是还不支持<merge /> 标签。

4.3.2 <include />标签

可以通过这个标签直接加载外部的xml到当前结构中,是复用UI资源的常用标签。

用法:将需要复用xml文件路径赋予include标签的Layout属性。

<includeandroid:id="@+id/cell1" layout="@layout/ar01" />

<includeandroid:layout_width="fill_parent" layout="@layout/ar01"/>

备注:

1.        include标签只有layout属性是必须的<includelayout="@layout/layout_ID"/>

2.        include标签若指定了ID属性,而你的layout也定义了ID,则你的layout的ID会被覆,<includeandroid:id="@+id/your_ID" layout="@layout/layout_ID"/>

3.        在include标签中所有的android:layout_*都是有效的。但前提是必须要写layout_width和layout_height两个属性,否则无效。

4.3.3 <merge />标签

它在优化UI结构时起到很重要的作用,目的是通过删减多余或者额外的层级,从而优化整个Android的Layout结构。

示例:

运行上边的layout,然后启动tools> hierarchyviewer.bat工具查看当前UI结构视图:

我们可以很明显的看到出现了两个framelayout节点,这两个意义完全相同的节点造成了资源浪费

这时候就要用到<merge/>标签来处理类似的问题了。我们将上边xml代码中的FrameLayout标签替换成merge:

运行程序后显示的效果是一样的,可是通过hierarchy viewer查看的UI结构是有变化的,当初多余的 FrameLayout节点被合并在一起了,或者可以理解为将merge标签中的子集直接加到Activity的FrameLayout跟节点下(所有的Activity视图的根节点都是FrameLayout)。。

备注:

1.        <merge />只可以作为layout的根节点;

2.        当需要Inflate扩充的layout本身是由merge作为根节点的话,需要将被导入的layout置于viewGroup中,同时需要设置attachToRoot为true。

4.4 界面初始化和数据懒加载

在编码实践中,我们一般会把界面layout的初始化任务放在onCreate中,但与此同时,经常会有数据相关的操作,包括使用数据库、SP等存取标记值、数据的初始化、服务的初始化,甚至网络数据请求等,使得初始化方法中存在加载内容过多、过重、过散问题,使得UI呈现的效率变慢。

从用户体验的角度来说,应该让UI初始化越早完成越好;从编码的角度来说,业内的实践一般是尽可能的将UI和数据的初始化业务分离开来,优先完成UI的初始化,根据数据的返回再做刷新。具体至少可从以下方面进行规范。

4.4.1 严格控制初始化方法中操作数据库、SharePreference或其它文件

常见需求是,在初始化时有不少轻量级操作来获取UI加载策略,主要包括internalStorage标记存取。

规范要求

1.        单个界面整个初始化过程中,不超过2个轻量级标记操作,且集中调用;

2.        如果是两个界面交互,尽可能使用传值,避免存取消耗;

3.  优先使用SP标记,经过sdk封装,性能较好。

但是以上控制的前提是,足够轻量级,不会造成耗时阻塞,可采用StrictMode进行检查。

4.4.2 初始化加载data选好线程和时机

尽可能把view先加载并显示出来,具体为走完onResume方法。因此,可以清理一下initData等其它业务,把能够懒加载的全部extra出去,到显示出界面再加载。

对于lazyLoad的时机选择问题,比较简单常规的做法是,在onCreate(Fragment的 onCreateView)中开辟{子线程}。这在一般的界面是没问题的,但是如果界面过重一些多个Fragment还是有可能影响界面加载,如某个控件/Fragment的data很快返回后又调度主线程加载其响应UI元素,影响了其它原UI元素的初始化。

4.4.2.1 Activity/Fragment生命周期方法执行顺序

如下对一个界面加载一个Fragment和多个Fragment的生命周期的执行打印日志,同时扩展一个底层的周期接口getDecorView().post():

示例代码:MyActivity的onCreate如下,Fragment的onActivityCreated同理;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my);
    Log.e(TAG, "onCreate()----------");
    // 初始化各种控件
    initViews();

    // 初始化mTitles需要的数据
    initData();

    // 对各种控件进行设置、适配、填充数据
    configViews();
    getWindow().getDecorView().post(new Runnable() {
        @Override
        public void run() {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    // mainThread
                    Log.e(TAG, "getDecorView().post(): run currentThread()= " + Thread.currentThread().getName());
                    showDialog();

                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            Log.e(TAG, "getDecorView().post(): run currentThread= " + Thread.currentThread().getName());
                            lazyLoadData();
                        }
                    }).start();
                }
            });
        }
    });
}

 

Log打印:

Activity带1个fragment

E/MyActivity: onCreate()----------

E/MyActivity: onStart()----------

E/MyActivity: onResume()----------

E/MyFragment0: onAttach()----------

E/MyFragment0: onCreateView()----------

E/MyFragment0:onActivityCreated()----------

E/MyFragment0: onResume()----------

E/MyFragment0: initData() run----------

E/MyActivity: getDecorView().post(): run

E/MyFragment0: getDecorView().post()run----------

Activity带5个fragment

E/MyActivity: onCreate()----------

E/MyActivity: onStart()----------

E/MyActivity: onResume()----------

E/MyFragment0: onAttach()----------

E/MyFragment0: onCreateView()----------

E/MyFragment0:onActivityCreated()----------

E/MyFragment0: onResume()----------

E/MyFragment1: onAttach()----------

E/MyFragment1: onCreateView()----------

E/MyFragment0: initData() run----------

E/MyFragment1:onActivityCreated()----------

E/MyFragment2: onAttach()----------

E/MyFragment2: onCreateView()----------

E/MyFragment1: initData() run----------

E/MyFragment2:onActivityCreated()----------

E/MyFragment3: onAttach()----------

E/MyFragment3: onCreateView()----------

E/MyFragment2: initData() run----------

E/MyFragment3:onActivityCreated()----------

E/MyFragment4: onAttach()----------

E/MyFragment4: onCreateView()----------

E/MyFragment3: initData() run----------

E/MyFragment4:onActivityCreated()----------

E/MyFragment1: onResume()----------

E/MyFragment2: onResume()----------

E/MyFragment3: onResume()----------

E/MyFragment4: onResume()----------

E/MyFragment4: initData() run----------

E/MyActivity: getDecorView().post(): run

E/MyFragment0: getDecorView().post()run----------

E/MyFragment1: getDecorView().post()run----------

E/MyFragment2: getDecorView().post()run----------

E/MyFragment3: getDecorView().post()run----------

E/MyFragment4: getDecorView().post() run----------

 

可以看到使用getDecorView().post出去的任务,执行时机由Android底层控制可以保证在当前界面所有view走完onResume之后执行,无论在Activity还是Fragment,都可以作为有懒加载需要的一个best practice接口。

基本使用方式为

getWindow().getDecorView().post(newRunnable(){handler.post(new Runnable(){lazyLoadData();})})

备注:此时lazyLoadData()中仍为主线程

4.4.2.2 大型列表数据执行懒加载

有列表的界面,先加载空列表再刷新列表内容,即使是从缓存取数据,拉取和解析,也是需要数十到百多ms不等时间的,且数据量大小未知,在初始化界面时事先加载数据也是可能很影响加载性能的。

具体执行:

1.        数据执行时机:上述getDecorView().post接口回调周期,是一个推荐的选择;

2.        回调刷新:先通过Adapter进行数据引用对象的绑定,回调时封装数据,利用AbsListView或者RecyclerView的刷新notify刷新接口;

4.4.2.3 Dialog等可以懒加载的一些UI显示

理想状态下,也从onCreate剥离到getWindow().getDecorView().post中,但是得做好Dialog未加载前可能出现的用户点击事件的防护,这就要求开发者在编码中做好边界和状态维护,使程序具有更好的稳定性。

 

4.5 列表ConvertView与RecyclerView

4.5.1 AbsListView与ConvertView的复用

传统的AbsListView,其BaseAdapter暴露的getView()接口中,提供了ConvertView机制,将那些不再被用的ITEM的convertView重新给即将显示的ITEM使用,如此避免了大量不必要的ItemView的创建,极大的节约性能开销。

其复用机制如下图所示:

规范的做法是:

1.        当convertView为空,即初始化创建时,我们就将生成的布局利用setTag()保存在convertView中,当convertView利用回收机制回收过来让我们再次使用时,我们通过getTag()将保存的布局取出来,重新将布局里的各个控件重新赋值就可以了;

2.        对于ITEM的布局,为了便于操作,将ITEM的细节控件统一使用ViewHolder进行组合封装,然后将ViewHolder作为tag整体set给convertView。

4.5.2 RecyclerView

RecyclerView是support.v7包中的控件,可以说是ListView和GridView的增强升级版,从2014年发布到现在,使用已经相当普遍。

RecyclerView的用法和ListView类似,但是其内部自行维护了ViewHolder,同时支持局部刷新,使用起来更为高效。

因此,规范要求,新建的列表必须启用RecyclerView以及基于其扩展的组件;原有的列表在重构时逐步转换为RecyclerView及其扩展组件。

具体实现示例已经非常普遍,不再细表,参考官方文档,或者文章:《RecyclerView使用完全解析 体验艺术般的控件》。

4.6 列表滑动状态与加载图片优化

如图片加载篇中的列表加载状态集成小节所述,列表Fling快速滑动状态下,如果ITEM中有图片控件,那么此时的图片加载应该得到暂停,在滚动状态恢复正常或停止的时候,再执行图片的加载。

从列表的角度来说,要求实现滚动监听接口,然后调用图片加载库的暂停或者恢复接口。

4.7 自定义View

自定义View一般有两种定义方式:

1.        通过layout组合和扩展形成新的ViewGroup;

2.        通过创建新的类,复写View.onDraw()等接口,实现新的View。

其中第二种方式,涉及两个重要的类对象:Paint和Canvas。测量和绘制的实现过程比较复杂,因此,也是性能上比较关注的点。下面就其中比较容易出现的问题及其规范进行阐述,每一个这样的问题,都可能影响GPU达成60FPS的渲染目标。

4.7.1 在不必要的时候刷新View;

View的invalidation相关刷新方法,经常被自定义View的开发者过度使用,毕竟,这是一套在任何时候都可以非常快速的呈现和刷新view表现的方法。

实际上,调用View.invalidate()和View.requestLayout()之类方法,将强行刷新整个hierarchy和引发整体重绘,是一个非常重的开销。我们应该对调用这类的方法保持谨慎,避免影响整体UI渲染,减轻GPU的负担。

4.7.2 对不可见的像素进行了绘制,过度绘制;

Overdraw(过度绘制)是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次,无用图层绘制在底层,造成不必要的浪费。可使用>开发者选项>调试GPU过度渲染观察。

避免过度绘制需要从多方面考虑,包括本篇章之前所提到的各种layout规范,这里简单总结一下:

l  移除Window默认的Background

l  getWidow.setBackgroundDrawable(null);

l  移除XML布局文件中非必需的Background

l  按需求显示占位背景图片

l  减少布局嵌套

l  使用三大xml标签merge节点

l  这里针对自定义View,补充一项:ClipRect

我们自定义View,重写onDraw()方法,方法里对View的操作将会重绘,然而我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行。

API:canvas.clipRect()

应用场景:比如像下图的侧滑导航菜单,滑出的过程中,右边主页内容区域不需要绘制。

 

4.7.3 避免在onDraw()方法里面执行导致内存分配的操作

首先onDraw()方法是执行在UI线程的,在UI线程尽量避免做任何可能影响到性能的操作。虽然分配内存的操作并不需要花费太多系统资源,但是这并不意味着是免费无代价的。设备有一定的刷新频率,导致View的onDraw方法会被频繁的调用,如果onDraw方法效率低下,在频繁刷新累积的效应下,效率低的问题会被扩大,然后会对性能有严重的影响。

规范要求是,把分配操作移动到onDraw()方法外面,其中最典型的例子就是,在onDraw()里面需要创建画笔,那么请检查该操作,把new Paint()的语句移动到外面。


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值