Android App优化之Layout怎么摆

系列文:

  1. 背景:Android App优化, 要怎么做?
  2. Android App优化之性能分析工具
  3. Android App优化之提升你的App启动速度之理论基础
  4. Android App优化之提升你的App启动速度之实例挑战
  5. Android App优化之Layout怎么摆
  6. Android App优化之ANR详解
  7. Android App优化之消除卡顿
  8. Android App优化之内存优化
  9. Android App优化之持久电量
  10. Android App优化之如何高效网络请求

优化完App的启动速度, 接下来我们要关注的就是UI布局怎么更高效了.

欲善其事, 先利其器. 分析布局, 就不得不用到Hierarchy Viewer了.

本文工具使用皆以GithubApp的详情界面RepoDetailActivity为例说明.
为了不影响阅读体验, 对应的布局文件activity_repo_detail.xml的代码放在文末

1, Hierarchy Viewer怎么用

Hierarchy发音 [美: 'haɪərɑrki] [英: 'haɪərɑːkɪ] 层次结构的意思.
之前一直念不顺这个单词Hierarchy, 就简称为H Viewer了. 下文就这么简称吧.

官网描述, H Viewer是用来分析调试和优化我们的UI的一个图形化工具. 它会展示当前界面的View层级.

1.1 启用H Viewer

比较早接触Android开发的同学可能知道, H Viewer只能在root过的机器才能使用. 主要是在没有root过的机器中view server这个服务是没有开启的. H Viewer就无法连接到机器获取view层级信息.

正所谓高手在民间, 大家都尝试在未root的机器中启用view server来使用H Viewer. 最具代表性的就是romainguy的ViewServer, 只需集成少量代码到你的Activity, 相当于在手机端开启了view server服务, 建立socket通道与PC端的H Viewer通信.

此工程被Android官网吸收, 作为开启H View的方案之一.

完整开启H Viewer的套路如下:

  1. 手机开启开发者模式, USB调试.
  2. 根据手机的Android系统版本:
    • 4.0及以下, 没有root. 使用上述的开源工程ViewServer提供的方式.
    • 4.0及以下, 已经root. 无需其他额外设置.
    • 4.1及以上. 需要在PC端设置ANDROID_HVPROTO环境变量.

设置系统环境变量: ANDROID_HVPROTO, 值为ddm
具体设置系统环境变量根据PC系统不同而异.

做完上述配置后, 你就可以打开H Viewer了, 打开DDMS, 如下操作进入H Viewer界面:


ddms_open_hviewer

1.2 H Viewer界面详解

GithubApp的详情界面RepoDetailActivity为例说明:

Snip20160902_1.png

界面分为四个部分:

  1. Window
    显示当前连接的设备和供分析的界面. 可手动选择.

  2. Tree View
    树状图的形式展示该Activity中的View层级结构. 可以放大缩小, 每个节点代表一个View, 点击可以弹出其属性, 当前值, 并且在LayoutView中会显示其在界面中相应位置.
    Tree View是我们主要要分析的视图.

  3. Tree Overview
    Tree View的概览图. 有一个选择框, 可以拖动选择查看. 选中的部分会在Tree View中显示.

  4. Layout View
    匹配手机屏幕的视图, 按照View的实际显示位置展示出来的框图.

1.3 H Viewer参数解读

  1. 通过Tree View可以很直观的看到View的层级.
  2. 点击Tree View的RepoItemView这个节点:
14728281715494.jpg

关于三个小圆点的性能指示, 在App优化之性能分析工具一文中有提到, 再强调一遍:

三个小圆点, 依次表示Measure, Layout, Draw, 可以理解为对应View的onMeasure, onLayout, onDraw三个方法.

  • 绿色, 表示该View的此项性能比该View Tree中超过50%的View都要快.
  • 黄色, 表示该View的此项性能比该View Tree中超过50%的View都要慢.
  • 红色, 表示该View的此项性能是View Tree中最慢的.

如果你的界面的Tree View中红点较多, 那就需要注意了. 一般来说:

1, Measure红点, 可能是布局中嵌套RelativeLayout, 或是嵌套LinearLayout都使用了weight属性.
2, Layout红点, 可能是布局层级太深.
3, Draw红点, 可能是自定义View的绘制有问题, 复杂计算等.

由上图, 可以看到我们的RepoItemView的三项指标都不合格, 证明其还有很多优化空间. 层级, 绘制都可以优化.

除了用H Viewer来做代码后分析, Android还提供了Lint, 在我们编写xml布局文件时就即时的给出一些相关提示.

2, Lint tool

打开RepoDetailActivity的布局文件activity_repo_detail.xml, 在Android Studio菜单栏中开启Lint检查:

14728313149102.jpg

选择当前文件:

14728313382536.jpg

会在下方弹出分析结果:

14728314908964.jpg

分析结果包括用法检测(例如版本特有属性), 国际化(字符串是否提取到strings.xml, Rlt支持等), 以及我们今天的主题---性能分析结果.

点开"Android -> Lint -> Performance"项, 可以看到关于布局性能的建议项. 此例中是说ScrollView的父级LinearLayout是不必要的.

3, 怎么优化你的布局

通过以上工具的使用和分析, 也基本能找到布局的一些常见的好与不好的了.

正所谓授之以鱼不如授之以渔. 在此也就不太详细去讲怎么优化了, 几点建议, 大家自行实践吧:)

尽量减少布局层级和复杂度

  1. 尽量不要嵌套使用RelativeLayout.
  2. 尽量不要在嵌套的LinearLayout中都使用weight属性.
  3. Layout的选择, 以尽量减少View树的层级为主.
  4. 去除不必要的父布局.
  5. 善用TextView的Drawable减少布局层级
  6. 如果H Viewer查看层级超过5层, 你就需要考虑优化下布局了~

善用Tag

  1. <include>
    使用include来重用布局.
  2. <merge>
    使用<merge>来解决include或自定义组合ViewGroup导致的冗余层级问题. 例如本例中的RepoItemView的布局文件实际可以用一个<merge>标签来减少一级.
  3. <ViewStub>

ListView优化

  1. contentView复用
  2. 引入holder来避免重复的findViewById.
  3. 分页加载

4, 附示例代码

因github上的源码会持续更新, 特留对应代码在此.

activity_repo_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/root_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/md_white_1000"
    android:orientation="vertical"
    android:padding="@dimen/dimen_10">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        android:scrollbars="none">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <com.anly.githubapp.ui.widget.RepoItemView
                android:id="@+id/repo_item_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/md_grey_300"
                android:elevation="@dimen/dimen_2"/>

            <LinearLayout
                android:id="@+id/contributor_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/dimen_10"
                android:orientation="vertical"
                >

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/dimen_40"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:background="@drawable/button_bg"
                    android:paddingLeft="@dimen/dimen_10">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:gravity="center_vertical"
                        android:text="{oct-organization} Contributors"/>

                    <TextView
                        android:id="@+id/contributors_count"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/dimen_40"
                        android:gravity="center_vertical"/>

                </LinearLayout>

                <android.support.v7.widget.RecyclerView
                    android:id="@+id/contributor_list"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/dimen_60"
                    android:layout_marginTop="@dimen/dimen_2"
                    />

            </LinearLayout>

            <LinearLayout
                android:id="@+id/fork_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/dimen_10"
                android:orientation="vertical"
                >


                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/dimen_40"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:background="@drawable/button_bg"
                    android:paddingLeft="@dimen/dimen_10"
                    >

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:gravity="center_vertical"
                        android:text="{oct-gist_fork} Forks"/>

                    <TextView
                        android:id="@+id/forks_count"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/dimen_40"
                        android:gravity="center_vertical"/>

                </LinearLayout>

                <android.support.v7.widget.RecyclerView
                    android:id="@+id/fork_list"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/dimen_60"
                    android:layout_marginTop="@dimen/dimen_2"
                    />

            </LinearLayout>

            <LinearLayout
                android:id="@+id/code_layout"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dimen_40"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:layout_marginTop="@dimen/dimen_10"
                android:background="@drawable/button_bg"
                android:paddingLeft="@dimen/dimen_10">

                <TextView
                    android:id="@+id/code_label"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/dimen_40"
                    android:gravity="center_vertical"
                    android:text="{oct-file_code} Code"/>

            </LinearLayout>

            <LinearLayout
                android:id="@+id/readme_layout"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dimen_40"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:layout_marginTop="@dimen/dimen_10"
                android:background="@drawable/button_bg"
                android:paddingLeft="@dimen/dimen_10">

                <TextView
                    android:id="@+id/readme_label"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/dimen_40"
                    android:gravity="center_vertical"
                    android:text="{oct-info} README"/>

            </LinearLayout>

        </LinearLayout>

    </ScrollView>
</LinearLayout>

com.anly.githubapp.ui.widget.RepoItemView对应的布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/dimen_10">

        <TextView
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="left|center_vertical"
            android:maxLines="1"
            android:text="@string/app_name"
            android:textColor="@android:color/black"
            android:textSize="@dimen/text_size_18"/>

        <TextView
            android:id="@+id/desc"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="left|center_vertical"
            android:maxLines="2"
            android:text="@string/app_name"
            android:textColor="@android:color/darker_gray"
            android:textSize="@dimen/text_size_12"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/dimen_5"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/image"
                android:layout_width="@dimen/dimen_32"
                android:layout_height="@dimen/dimen_32"
                android:scaleType="centerInside"
                android:src="@mipmap/ic_launcher"
                android:visibility="visible"/>

            <TextView
                android:id="@+id/owner"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginLeft="@dimen/dimen_10"
                android:gravity="left|center_vertical"
                android:text="@string/app_name"
                android:textColor="@android:color/black"
                android:textSize="@dimen/text_size_14"/>

        </LinearLayout>

        <View
            android:layout_marginTop="@dimen/dimen_5"
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="@color/grey"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/dimen_32"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingTop="@dimen/dimen_10">

            <TextView
                android:id="@+id/update_time"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="left|center_vertical"
                android:text="@string/app_name"
                android:textColor="@android:color/black"
                android:textSize="@dimen/text_size_12"
                />

            <View
                android:layout_width="1px"
                android:layout_height="match_parent"
                android:background="@color/grey"/>

            <LinearLayout
                android:id="@+id/star_view"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center"
                android:orientation="horizontal">

                <ImageView
                    android:id="@+id/star_icon"
                    android:layout_width="@dimen/dimen_16"
                    android:layout_height="@dimen/dimen_16"
                    android:scaleType="centerInside"
                    android:src="@drawable/ic_star"/>

                <TextView
                    android:id="@+id/star"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="@dimen/dimen_5"
                    android:gravity="center"
                    android:text="@string/app_name"
                    android:textColor="@android:color/black"
                    android:textSize="@dimen/text_size_12"
                    />

            </LinearLayout>


        </LinearLayout>

    </LinearLayout>

    <com.flyco.labelview.LabelView
        android:id="@+id/label_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        app:lv_background_color="@color/md_yellow_500"
        app:lv_gravity="TOP_RIGHT"
        app:lv_text="TEST"
        app:lv_text_size="@dimen/text_size_12"/>
</FrameLayout>

优化不同于做功能, 可能分析的多, 出的成果少~ 比较枯燥, 然而优化也是App发展的必经之路, 欢迎大家分享经验.

转载请注明出处, 欢迎大家分享到朋友圈, 微博~

推荐阅读更多精彩内容