Android 开发收集的一些东西

Android性能优化——优化布局(Improving Layo

2016-11-02  本文已影响735人  silentleaf

本片是对Android的性能优化的一系列文章中的其中一篇的翻译,原文地址如下

https://developer.android.com/training/improving-layouts/optimizing-layout.html#Inspect

一.前言

布局是Android应用直接影响用户体验的一个重要的部分,如果优化的不好,那么应用很可能频繁的出现内存不足以及界面响应过慢的问题。Android的SDK已经提供了一系列的工具用于帮助开发者找出布局中的一些问题,这里会叙述一系列的案例并结合这些工具的使用来帮助开发者实现一个流畅的布局界面。

本片文章将从下面几个部分进行叙述

二.优化布局层级

使用SDK提供的基础Layout控件就一定能构建出一个高效的布局结构是一个常见的误区,实际上我们布局中使用的所有控件在运行中都是需要经历初始化、布局、绘制三个步骤的。那么使用嵌套的LinearLayout会产生非常深的视图层级,三个步骤也就被反复执行,更有如果在这些嵌套的LinearLayout中还使用layout_weight参数的话更容易造成性能问题,因为每一个子布局都需要测量(Measure)两次。这一点是非常重要的,尤其是在一些经常被复用的布局视图如ListView或者GridView中。
下面将通过例子的方式展示Hierarchy Viewer以及Layoutopt的使用

检查布局

Android SDK中有一个叫做『Hierarchy Viewer』的工具,它能够帮助你在应用运行的过程中去分析应用的布局,以便去发现布局性能的瓶颈。
Hierarchy Viewer的使用非常简单,首先在模拟器或者已经连接上的真机中选择需要分析的进程,然后就可以看到布局树(Layout Tree)了。布局树中每一块都用红黄绿三色信号灯描述当前的测量、布局、绘制的性能,用于帮助你发现一些潜在的问题。
比如Figure 1展示了一个简单的布局,这个布局由左侧的Bitmap以及右侧竖直排序的两个TextView构成。这是一个用于ListView的Item的经典结构,因此这一布局的性能非常重要,因为它将被复用很多次,同时它的优化带来的性能提升也非常大。

Figure 1 - ListView中Item布局示意

Hierarchy Viewer在SDK目录下的tools目录下,打开之后会发现它展示了所有链接上的设备以及在它们之上运行的一些项目。点击Load View Hierarcy可以去查看选中的项目(注:我这边贴一张图作为补充,如下所示,可以看到我这里链接了一款乐视的手机,其中可以选择进行分析的项目有多个)

点击Load View Hierarcy之后会出现Figure 2所示的实际结果

Figure 2 - 分析结果

在Figure 2中我们可以看到这里有三层视图结构,其中在展示文本项的时候似乎出现了一点问题。点击其中的每一个小块可以看到测量,布局,绘制的详细耗时情况,如Figure 3所示,这样就可以有针对性的对某一部分进行优化。

Figure 3 - 点击后详情展示

因此ListView中每一项渲染的实际耗时情况如下

修正布局

通过Figure 2我们可以看到嵌套的LinearLayout造成了布局上的性能问题,通过将嵌套的布局拆开保证布局树的扁平化是优化的一个方向。RelativeLayout作为根节点可以达到我们的目的,实际上使用RelativeLayout之后我们原有的视图层级从3层降到了2层,分析结果也变成Figure 4所示的样子

Figure 4 - RelativeLayout实现

此时,渲染ListView的一项耗时情况如下

使用Lint

在布局文件上使用lint工具用于发现布局上可以优化的点是一个非常好的习惯。lint由于具有非常强大的功能,因此替代了以前使用的Layoutopt工具,一些简单的lint规范案例如下

使用lint的另一个好处是它已经集成到Android Studio中了,lint将在应用编译的时候自动运行。使用Android Studio你可以在构建某一特定的变种版本(Build variant)或者全部变种版本的时候执行lint检查。
你可以自己配置lint的检查文件去自定义一些内容,入口在Android Studio的File>Settings>Project Settings中,如Figure 5所示

Figure 5 - lint检查配置页

lint可以对代码提供一些建议,同时也能帮我们自动修复一些问题。



项目问题

HV工具可以很好的帮助我们发现布局中的一些问题,具体使用可以参考Optimizing Your UI,同时lint的能力不仅仅体现在布局优化上,想要运行lint,也可以直接点击Analyze>Inspect Code,最终结果会分类目详细展示出来。具体如何使用lint,可以参考Improve Your Code with Lint

三.利用<include/>复用布局

虽然Android提供了很多的控件用于帮助我们在在布局文件中进行元素复用,但是实际项目中我们也许还需要更大层面上的复用元素,比如一个特殊的布局。为了高效的复用整个布局,你可以使用<include/><merge/>标签将已有布局嵌入其他布局中。
使用这个能力可以让你创造出非常复杂的可复用布局,比如一个带有yes/no的按钮板,带有文字描述的进度条。这同样意为着你项目中的任何一个布局元素都可以分开进行管理,当需要的时候你只要用到include就行。

创造可复用布局

如果你已经知道哪些布局需要被重用,那只需要新建一个xml文件,然后将被重用的布局写入进去就可以了。比如下面就是一个可以被重用的布局,文件名为titlebar.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent" 
    android:layout_height="wrap_content"    
    android:background="@color/titlebar_bg">
        <ImageView android:layout_width="wrap_content"               
            android:layout_height="wrap_content"               
            android:src="@drawable/gafricalogo" />
</FrameLayout>

使用<include>标签

在你需要的地方用<include />标签添加之前定义的布局即可重用布局。比如我需要重用上面定义的titlebar.xml布局,代码可以这样

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

include中所有layout开头的属性(android:layout_*)都可以被重写,但他们只有在重写了android:layout_heightandroid:layout_width之后才生效

使用<merge>标签

merge标签可以帮助我们剔除布局层级中无用的视图层。比如你有一个LinearLayout作为根视图的布局,它里面需要有一个包含两个连续视图(比如按钮)的可重用布局,这个可重用布局你需要重新定义它的根视图,比如你会使用LinearLayout。但是这时候使用该LinearLayout作为可重用布局的根视图会导致增加一个毫无用处的视图层级。为了避免这种现象,可以使用<merge>作为可重用布局的根视图,比如

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/add"/>

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/delete"/>

</merge>

此时如果你将这个可重用布局使用<include>标签包含至其他布局文件中,系统会忽略merge元素然后将两个Button直接放置在include标签所在的地方。

注意点

<include>
<merge>

项目问题

项目中使用include的地方有四十多处,大家对这个标签其实也比较属性,相反merge的话使用的地方仅有一处,建议可以结合上面的注意点尝试使用。

四.懒加载View

你的布局中可能存在很少情况下才用到的复杂布局,比如单条详情、进图条或者是一些撤销消息等等,这些布局可以只在你需要的时候才加载以提升布局的渲染速度。

定义ViewStub

ViewStub是一个轻量级的视图,它不参与绘制也不参与任何的布局工作。因此,它在视图层级的构建中消耗的资源是非常小的。每一个ViewStub在使用时只需要通过android:layout去定义它需要加载布局文件即可。
下面给出的ViewStub承载了一个透明的进度条,它只在特定情况下才需要展现给用户。

<ViewStub
    android:id="@+id/stub_import"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom" />

加载ViewStub布局

当我们需要让ViewStub承载的视图展现时,只需要通过调用setVisibility(View.VISIBLE)或者inflate()方法即可。

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

一旦ViewStub被可见或者被布局了,那么它就从视图层级中剥离出来,取代ViewStub存在于视图层级的是android:layout属性所指定的布局,该布局的id可以通过android:inflatedId指定。

这里和include一样,android:inflatedId属性也会覆盖layout中根视图的id。

注意点

ViewStub是一个比较特殊的View,与渲染相关的方法它的实现基本都是空实现,因此能节约很多性能。除此之外它的其他特性要求我们使用时要稍加注意。

项目问题

项目中可能大家更习惯使用Visibility的切换而不是ViewStub。如果在布局中你有需要设置可见性的地方,不妨思考是否需要频繁切换它的可见状态,是否需要懒加载,如果用户见到的可能性不大或者它本身也不经常切换自身的可见性,那么可以考虑使用ViewStub,比如『点击展开详情』这种类似的功能。

五.让ListView起飞

让ListView更流畅的最重要的一点是要牢记让主线程从繁杂的任务任务中解放出来,确保磁盘访问、网络请求或者数据库操作是在单独的线程中的。可以使用StrictMode去验证你的app是否遵循这一点。

使用后台线程

使用后台线程(也成为工作线程)去处理原本打算放在主线程中的复杂逻辑,以保证主线程更专注的处理UI绘制工作。通常情况下,使用AsyncTask提供了一个非常简单的方式用于帮助你在主线程之外处理逻辑。AsyncTask自动的将所有的execute()请求队列化,然后依次执行,当然这一策略不会影响你自己创建的线程池。
在下面的样例代码中,AsyncTask用于在后台加载图片,并且在图片加载完成后提供给主线程使用,它允许在图片加载过程中使用进度条做界面展示。

// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<ViewHolder, Void, Bitmap>() {
    private ViewHolder v;

    @Override
    protected Bitmap doInBackground(ViewHolder... params) {
        v = params[0];
        return mFakeImageLoader.getImage();
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (v.position == position) {
            // If this item hasn't been recycled already, hide the
            // progress and set and show the image
            v.progress.setVisibility(View.GONE);
            v.icon.setVisibility(View.VISIBLE);
            v.icon.setImageBitmap(result);
        }
    }
}.execute(holder);

从3.0开始,AsyncTask提供了额外的方式以允许你提高在多核手机上的并发处理能力,此时你需要用executeOnExecutor()替换之前的execute()方法。(注:AsyncTask在刚开始是以单独线程去处理所有请求的,从1.6开始被修改成以线程池的方式处理所有的请求,但是从3.0开始又改成了单独线程处理所有请求,想想谷歌是挺好玩的。不过正如这里说3.0版本之后你可以使用executeOnExecutor方法去制定自己的AsyncTask线程池。)

使用View Holder保持视图对象

你可能要使用findViewById()方法去获取视图对象,但是如果getView时也这么做的话,那么滚动的过程中就会触发N多次该方法的调用,这一点即便在Adapter提供了滚动过程中使用复用视图以避免重复inflate也无法得到改观。一个比较好的方法去避免N多次调用findViewById是使用『view holder』。
一个ViewHolder对象存储了ListView中的Item的Layout中所需要的视图组件,所以使用了ViewHolder之后你可以直接访问到它们而无需多次调用findViewById。为了使用ViewHolder首先你需要定义自己的类

static class ViewHolder {
  TextView text;
  TextView timestamp;
  ImageView icon;
  ProgressBar progress;
  int position;
}

然后在需要的时候创建并存储它

ViewHolder holder = new ViewHolder();
holder.icon = (ImageView) convertView.findViewById(R.id.listitem_image);
holder.text = (TextView) convertView.findViewById(R.id.listitem_text);
holder.timestamp = (TextView) convertView.findViewById(R.id.listitem_timestamp);
holder.progress = (ProgressBar) convertView.findViewById(R.id.progress_spinner);
convertView.setTag(holder);

现在你就可以直接访问到所有的视图了,节约了很多资源。

项目问题

后台线程还有ViewHolder,大家使用的已经很多了,应该没有特别大的问题。

上一篇 下一篇

猜你喜欢

热点阅读