Android 的性能 III-提升Layout性能

来源:互联网 发布:网络危机管理 编辑:程序博客网 时间:2024/06/05 16:54

概述:

Layout是Android APP的关键部分, 会直接影响用户体验. 如果实现的有问题, 你的layout可以导致APP内存不足并伴随反应速度很慢的UI. Android SDK内有一些工具来帮助你识别出影响layout性能的问题, 本文将会介绍如何使用这些工具来实现最少内存占用的平滑的接口.

本文包括:

优化layout层次结构;

使用<include>重用layout;

按需加载view;

使ListView滚动更加平滑;

优化layout层次结构:

有一个常见的误解就是, 使用基本布局结构是最高效的. 但是每个添加到APP中的组件和layout都需要初始化, 布局和绘制. 栗如, 使用嵌套的LinearLayout实例会导致过深的视图层次结构. 此外, 使用layout_weight的嵌套的LinearLayout会尤其的昂贵, 因为每个child都需要被测量两次. 当出现重复的时候这尤其严重, 比如在使用ListView或者GridView的时候.

检查你的布局:

Android SDK工具箱中包含一个叫做HierarchyViewer的工具, 它可以让你在APP运行时分析layout. 使用该工具可以帮助你发现layout性能中的瓶颈.

Hierarchy Viewer可以让你选择正在运行的进程,然后显示其layout树. 每个块儿上的红绿色代表其测量值, Layout和绘制性能, 帮助你识别潜在的问题. 栗如, 下图演示了ListView中一个Item使用的layout. 它在左侧显示了一个小图片, 在右侧显示了堆放起来的文字. 尤其重要的是, 该layout将会被使用多次, 这意味着对它的优化也可以提供加倍的性能.


图1, 表示在ListView中的一个项的概念布局.

Hierarchyviewer工具在<sdk>/tools/目录下. 当它打开的时候, Hierarchyviewer显示一个可用设备的列表以及正在运行的组件. 点击Load View Hierarchy来显示选中组件的layout层次结构. 栗如, 下图展示了图1中的布局的layout:


图2, 描述的是图1中的布局层次结构, 它使用了嵌套的LinearLayout.


图3, 点击一个节点之后会显示其性能.

在图2中, 你可以看到有3级层次结构, 并且在文本条目中有一些问题. 点击条目来显示过程中每个阶段的时间消耗(图3). 这时哪个条目在测量布局和显示过程中耗费了最多的时间并值得花时间优化就变得显而易见了. 在该布局中渲染一个复杂列表项的时间消耗是:

l  Measure: 0.977ms

l  Layout: 0.167ms

l  Draw: 2.717ms

修正你的布局:

因为上面的布局性能被嵌套的LinearLayout拉低了, 所以可以通过”压扁”布局来优化其性能—使得布局变得浅而宽而不是窄而深. 一个RelativeLayout作为根节点就可以满足这样的布局. 所以, 当该设计被修改为使用RelativeLayout的时候, 你可以看到布局变成了一个两级的层次结构. 新布局看起来是这样的:


图4, 将图1中的布局改为用RelativeLayout作为根节点的布局. 现在, 渲染一个列表项需要消耗:

l  Measure: 0.598ms

l  Layout: 0.110ms

l  Draw: 2.146ms

可能上面的效率提升看起来不值一提, 但是当放入列表中计算的时候, 这些时间就会被加倍. 大多数的时间差异是由于在LinearLayout设计中使用了layout_weight, 它会降低测量的速度. 这只是一个简单的栗子, 你应该仔细的考虑是否有必要使用layout_weight.

使用Lint工具:

对你的layout文件使用lint工具来查找可能的view层次结构优化是一个很好的习惯. Lint已经代替了Layoutopt工具并提供了更强大的功能. 一些lint规则的栗子如下:

l  使用合成drawable –包含了一个ImageView和一个TextView的LinearLayout作为合成drawable处理的时候会更加的高效.

l  合并根 frame – 如果一个FrameLayout是一个布局的根节点, 并且没有提供背景或者填充等,它可以被一个merge标签代替, 这样会稍微更加高效.

l  没用的leaf – 一个没有子项或没背景图的layout应该被移除(从它不可见时), 这样可以提供更加平面和高效的布局层次结构.

l  没用的parent – 如果一个layout只有一个child, 也不是一个ScrollView或者根节点, 也没有背景图, 那么可以将其移除, 直接放child, 这样可以降低性能消耗.

l  深层的layout: 太多层嵌套的layout对性能来说是很致命的. 应该考虑使用更平面的布局比如RelativeLayout或者GridView来提高性能, 默认情况下最大深度是10.

使用Lint的另一个好处是它是集成在Android Studio中的. Lint会在编译的时候自动运行. 你可以对特别的编译变量(build variants)运行lint检查, 或者所有的编译变量. 你可以在Android Studio中通过File>Settings>Project Settings选项来管理配置文件. 检查配置页面如下图:


图5, Lint检查配置.

Lint有能力自动修复一些问题, 并提供建议给那些不能修复的,可以直接跳转到相关的代码来检查.

使用<include/>来重用Layout:

尽管Android提供了大量的组件来提供小的可重用的互动标签, 但是你可能想要重用更大重量级的组件, 这样就需要特殊的layout. 要高效的重用整个layout, 你可以使用<include/>和<merge/>标签来将另一个layout嵌入到当前layout中.

重用layout是很有用的, 因为它可以让你创建复杂的可重用layout. 栗如, 一个yes/no按键面板, 或者一个带有描述文字的自定义的进度条. 它也意味着你的APP中的任何标签可以在多个layout中被提取, 独立管理, 然后包含在每个layout中. 所以你不但可以通过写一个自定义的view来创建自己的UI组件, 还可以甚至是更简单的创建一个可重用的layout文件.

创建一个可重用的layout:

如果你已经意识到了自己需要创建一个layout来重用, 那么创建一个新的XML文件并定义layout. 栗如, 下面是一个从G-Kenya代码实验室中获取的layout, 它定义了一个title bar, 可以包含在每个activity中(titlebar.xml):

<FrameLayoutxmlns: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>

根View应该明确表明你希望它如何显示在每个layout中.

使用<include>标签:

在你想要添加可重用组件的layout中, 添加一个<include/>标签. 栗如, 这还是一个从G-Kenya代码实验室中获取的layout文件, 它包含了上面的title bar:

<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的所有layout参数(任何android:layout_*属性). 栗如:

<include android:id="@+id/news_title"         android:layout_width="match_parent"         android:layout_height="match_parent"         layout="@layout/title"/>

但是, 如果你想要使用<include>标签重写layout属性, 你就必须重写android:layout_height和android:layout_width, 这样其它的layout属性才能生效.

使用<merge>标签:

<merge/>标签可以在包含另一个layout的时候帮助消除view层次结构中那些冗余的view组. 栗如, 如果你的主layout是一个垂直的LinearLayout,其中的两个相邻的view可以在多个layout中重用, 那么就重用这个layout, 并将其中的两个view放入它们自己的根view中. 但是, 使用另一个LinearLayout作为重用layout的根将会导致一个垂直的LinearLayout在另一个垂直的LinearLayout中. 嵌套的LinearLayout除了会降低UI性能之外没什么实际的意义. 要避免引入这样的冗余view组, 你可以使用<merge>标签作为根view来替代LinearLayout作为重用layout的根, 栗如:

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

现在, 当你将这个layout包含到另一个layout中(使用<include/>标签)的时候, 系统会忽略<merge>标签并直接将这俩按钮放在layout中, 就在<include/>标签的位置.

有必要的时候才加载View:

有时候你的layout可能会用到一些很少使用的复杂的view. 不管它们是条目详情, 进度指示器, 还是未读消息; 你可以通过在需要的时候才加载它们来减少内存占用并加快渲染速度.

定义一个ViewStub:

ViewStub是一个轻量级的view, 它没有尺寸并且不在layout中绘制任何东西. 因此, 它在view层中的显示和保留都十分的廉价. 每个ViewStub只是简单的需要包含android:layout属性到指定的layout就可以了. 下面的ViewStub是用于一个半透明的进度覆盖(progress overlay). 它应该只有在新的条目正在被导入APP的时候才显示:

<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);// orView importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

注意: inflate()方法一旦完成后会返回一个inflated的view. 所以如果你需要使用layout交互那么你并需要调用findViewById()方法.

一旦可见/inflated, ViewStub元素就不再是view层次结构的一部分了. 它会被inflated的layout代替, 根view的ID则是通过android:inflatedId属性指定的ID. 只有在ViewStub显示出来之前它的android:id才有用.

注意: ViewStub的一个缺点是它当前不支持<merge/>标签.

让ListView平滑的滚动:

让ListView平滑滚动的关键是保持APP的主线程在繁重的进程中保持空闲. 要确保你做的任何硬盘访问, 网络访问或者数据库访问都在独立的线程中执行. 要测试APP的状态, 可以启用StrictMode.

使用一个后台线程:

使用一个后台线程(“worker thread”)从主线程中将繁重的任务移除出去, 这样主线程就可以专注于绘制UI了. 在很多情况下, 使用AsyncTask可以提供一个简单的方式来在主线程外部完成工作. AsyncTask自动排列所有的execute()请求并串行的执行它们. 这种行为对于特定的进程是global的, 这意味着你不需要关注自己的线程池.

在下面的栗子中, 一个AsyncTask被用来在后台线程加载一个图片, 一旦完成就将它们应用到UI. 它还在加载的时候显示了一个进度条:

// Using an AsyncTask to load the slow images in a background threadnew 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);

从Android 3.0开始, 在AsyncTask中引入了一个新功能, 让你可以在多个进程中使用它. 你可以指定executeOnExecutor()而不是execute(), 一次可以执行多个请求, 这取决于可用核的数量.

将View对象放在View Holder中:

你的代码可能经常在ListView滚动时调用findViewById(), 但是它会降低性能. 甚至在Adapter返回一个inflated view用于回收的时候. 你依然需要查看元素并更新它们. 一种代替使用findViewById()的方式是使用”view holder”设计模式.

一个ViewHolder对象保存每个组件View在layout的标签域中, 这样你就可以直接访问它们而不用查找它们了. 首先, 你需要创建一个类来hold住view中的集合. 栗如:

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

然后在填充ViewHolder并保存在layout中:

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);

现在你可以轻松的访问每个view而不用查找它们了, 节省了宝贵的处理器周期.

 

参考: https://developer.android.com/training/improving-layouts/index.html

0 0
原创粉丝点击