Android应用开发中View绘制的一些优化点解析

purplekoala 发布于4月前 阅读1013次
0 条评论

 一个通常的错误观念就是使用基本的布局结构(例如:LinearLayout、FrameLayout等)能够在大多数情况下
   产生高效率 的布局。 显然,你的应用程序里添加的每一个控件和每一个布局都需要初始化、布局(layout)、
   绘制 (drawing)。举例来说:嵌入一个LinearLayout会产生一个太深的布局层次。更严重的是,嵌入几个使
   用 layout_weight属性的LinearLayout 将会导致大量的开销,因为每个子视图都需要被测量两次。这是反复解析
   布局文件时重要的一点,例如在ListView或者GridView中使用时。

 观察你的布局
 
     Android SDK 工具箱包括一个称作“ Hierarchy Viewer”的工具,它允许你去在你的应用程序运行时分析
  布局。通过使用这个工具,能帮助你发现你的布局效率上的瓶颈问题。
 
     “ Hierarchy Viewer”工具允许你在已连接的设备或模拟器中选择正在运行的进程,然后呈现出布局层次树
   (layout tree)。每个正方块下的交通灯(见下图) --- 红绿蓝表现出了在测量(measure)、布局(layout)、以及绘制
   (draw)过程中的效率值,这能帮助你定位潜在的问题。
 
        假设ListView 中的一项Item 存在如下(见图 1)布局 :

Android应用开发中View绘制的一些优化点解析

 “Hierarchy Viewer”工具可以在 <sdk>/tools路径下找到。当打开它时,“ Hierarchy Viewer”工具显示了
   所有可用的设备以及运行在这些设备上的进程。点击”Load View Hierarchy”来显示某个你选择的组件的UI布局
  层次。举例来说,图2展现了图1的布局层次树。

Android应用开发中View绘制的一些优化点解析

  在图2中,你可以直观看到这个三层的布局结构是存在一些问题的。点击项体现出了在每个测量(measure)、
    布局(layout)、以及绘制(draw)过程中的时间消耗(见图3)。很明显,该项(LinearLayout)花费了最长的时间去
    测量、布局、绘制,你应该花点精力去优化它们。

Android应用开发中View绘制的一些优化点解析

 完成该布局文件渲染的时间分别为:
                测量过程:0.977ms
                布局过程: 0.167ms
                绘制过程:2.717ms
 
 修改布局文件
 
       由于上图中布局效率的低下是因为一个内嵌的 LinearLayout控件,通过扁平化布局文件----让布局变得
  更浅更宽,而不是变得更窄更深层次 ,这样就能提升效率了。 一个RelativeLayout 作为根节点也能提供如上
  的布局效果(即图1)。 因此,  使用RelativeLayout 改变布局的设计,你可以看到现在我们的布局层次只有2层了。
  新的布局层次树如下:

Android应用开发中View绘制的一些优化点解析

  现在,完成该布局文件渲染的时间分别为:
                      测量过程:0.977ms
                      布局过程:0.167ms
                      绘制过程:2.717ms
  
      也许它只是一点点微小的改进,但这次它会被多次调用,因为是ListView会布局所有的Item,累积起来,
 改进后效果还是非常可观地。

      大部分的时间差异是由于使用了带有layout_weight 属性的LinearLayout ,它能减缓测量过程的速度。这仅仅
 是一个例子,即每个布局都应该合适地被使用以及你应该认真考虑是否有必要采用“layout_weight" 属性。
 
 使用Lint工具 

        一个好的实践就是在你的布局文件中使用Lint工具去寻求可能优化布局层次的方法。Lint已经取代了Layoutopt
  工具并且它提供了更强大的功能。一些Lint规则如下:

  1、使用组合控件 --- 包含了一个  ImageView 以及一个 TextView 控件的 LinearLayout 如果能够作为一个
     组合控件将会被更有效的处理。
 2、合并作为根节点的帧布局(Framelayout)  ----如果一个帧布局时布局文件中的根节点,而且它没有背景图片
  或者padding等,更有效的方式是使用<merge />标签替换该< Framelayout />标签 。

                                        
 3、无用的叶子节点----- 通常来说如果一个布局控件没有子视图或者背景图片,那么该布局控件时可以被移除
       (由于它处于 invisible状态)。
 
 4、无用的父节点 -----  如果一个父视图即有子视图,但没有兄弟视图节点,该视图不是ScrollView控件或者
    根节点,并且它没有背景图片,也是可以被移除的,移除之后,该父视图的所有子视图都直接迁移至之前父视图
    的布局层次。同样能够使解析布局以及布局层次更有效。
 
 5、过深的布局层次  ----内嵌过多的布局总是低效率地。考虑使用一些扁平的布局控件,例如 RelativeLayout、
      GridLayout ,来改善布局过程。默认最大的布局深度为10 。
 
 
     当使用Eclipse环境开发时,Lint能够自动解决一些问题,提供一些建议以及直接跳转到出错的代码中去核查。
  如果你没有使用Eclipse,Lint也可以通过命令行的方式运行。
  
  使用<include />标签复用布局文件
       尽管Android通过内置了各种各样的控件提供了微小、可复用的交互性元素,也许你需要复用较大的
     组件 ---- 某些特定布局文件 。为了更有效率复用的布局文件,你可以使用<include />以及<merge />
     标签将其他的布局文件加入到当前的布局文件中。

          复用布局文件是一种特别强大的方法,它允许你创建可复用性的布局文件。例如,一个包含“Yse”or“No”的
     Button面版,或者是带有文字说明的 Progressbar。复用布局文件同样意味着你应用程序里的任何元素都能从
     繁杂的布局文件提取出来进行单独管理,接着你需要做的只是加入这些独立的布局文件(因为他们都是可复用地)。
     因此,当你通过自定义View创建独立的UI组件时,你可以复用布局文件让事情变得更简单。

 1、创建一个可复用性的布局文件

        如果你已经知道复用布局的”面貌”,那么创建、定义布局文件( 命名以”.xml”为后缀)。例如,这里是一个来自
 G- Kenya codelab 的布局文件,定义了在每个Activity中都要使用的一个自定义标题 (titlebar.xml):由于这些
  可复用性布局被添加至其他布局文件中,因此,它的每个根视图(root View)最好是精确(exactly)的。

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


 2、使用<include />标签

          在需要添加这些布局的地方,使用<include />标签 。 例如,下面是一个来自G-Kenya codelab的布局文件,
  它复用了上面列出的“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 />节点中为被添加的布局文件的root View定义特别标识,重写所有layout参数即可(任何
  以“android:layout_”为前缀的属性)。例如:

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

 3、使用<merge />标签

        当在布局文件中复用另外的布局时, <merge />标签能够在布局层次消除多余的视图元素。例如,如果你的
   主布局文件是一个垂直地包含两个View的LinearLayout,该布局能够复用在其他布局中,而对任意包含两个View的
   布局文件都需要一个root View(否则, 编译器会提示错误)。然而,在该可复用性布局中添加一个LinearLayout
   作为root View,将会导致一个垂直的LinearLayout包含另外的垂直LinearLayout。内嵌地LinearLayout只能减缓
   UI效率,其他毫无用处可言。
            该复用性布局利用.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="horizontal"> 
 
 <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"/> 
</LinearLayout> 


      为了避免冗余的布局元素,你可以使用<merge />作为复用性布局文件地root View 。例如:
           使用<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 />节点。
  
   越少越好

        为了加速视图,从那些调用频繁的活动中减少不必要的代码。在OnDraw()方法中开始绘制,它会给你最大的
   效益。特别低,你也应该减少在onDraw()方法中的内存分配,因为任何内存分配都可能导致内存回收,这将会
   引起不连贯。 在初始化或者动画之间分配对象。绝不要在动画运行时分配内存。


        另一方面需要减少onDraw()方法中的开销,只在需要时才调用onDraw()方法。通常invalidate()方法会调用
  onDraw()方法,因此减少对invalidate()的不必要调用。如果可能,调用它的重载版本即带有参数的invalidate()
  方法而不是无参的invalidate()方法。该带参数的方法invalidate()能使draw过程更有效,以及减少对落在该矩形
  区域(参数指定的区域)外视图的不必要重绘 。

       注,invalidate()的三个重载版本为:
            1 、public void invalidate (Rect dirty)
            2、public void invalidate (int l, int t, int r, int b)
            3、public void invalidate ()


        另外的一个高代价的操作是布局过程(layout)。 任何时刻对View调用requestLayout()方法,Android UI 框架
  都需要遍历整个View树,确定每个视图它们所占用的大小。如果在measure过程中有任何冲突,可能会多次遍历
  View树。UI设计人员有时为了实现某些效果,创建了较深层次的ViewGroup。但这些深层次View树会引发效率
  问题。确保你的View树层次尽可能浅。


        如果你有的UI设计是复杂地,你应该考虑设计一个自定义ViewGroup来实现layout过程。不同于内置View控件,
  自定义View能够假定它的每个子View的大小以及形状,同时能够避免为每个子View进行measure过程。 PieChart
  展示了如何继承ViewGroup类。 PieChart带有子View,但它从来没有measure它们。相反,它根据自己的布局算法
  去直接设置每个子View的大小。
         
       如下代码所示:

/** 
 * Custom view that shows a pie chart and, optionally, a label. 
 */ 
public class PieChart extends ViewGroup { 
 ... 
 // 
 // Measurement functions. This example uses a simple heuristic: it assumes that 
 // the pie chart should be at least as wide as its label. 
 // 
 @Override 
 protected int getSuggestedMinimumWidth() { 
  return (int) mTextWidth * 2; 
 } 
 @Override 
 protected int getSuggestedMinimumHeight() { 
  return (int) mTextWidth; 
 } 
 
 @Override 
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  // Try for a width based on our minimum 
  int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); 
 
  int w = Math.max(minw, MeasureSpec.getSize(widthMeasureSpec)); 
 
  // Whatever the width ends up being, ask for a height that would let the pie 
  // get as big as it can 
  int minh = (w - (int) mTextWidth) + getPaddingBottom() + getPaddingTop(); 
  int h = Math.min(MeasureSpec.getSize(heightMeasureSpec), minh); 
 
  setMeasuredDimension(w, h); 
 } 
 
 @Override 
 protected void onLayout(boolean changed, int l, int t, int r, int b) { 
  // Do nothing. Do not call the superclass method--that would start a layout pass 
  // on this view's children. PieChart lays out its children in onSizeChanged(). 
 } 
  
 @Override 
 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
  super.onSizeChanged(w, h, oldw, oldh); 
 
  // 
  // Set dimensions for text, pie chart, etc 
  // 
  // Account for padding 
 
  ... 
 
  // Lay out the child view that actually draws the pie. 
  mPieView.layout((int) mPieBounds.left, 
    (int) mPieBounds.top, 
    (int) mPieBounds.right, 
    (int) mPieBounds.bottom); 
  mPieView.setPivot(mPieBounds.width() / 2, mPieBounds.height() / 2); 
 
  mPointerView.layout(0, 0, w, h); 
  onDataChanged(); 
 } 
 
} 


使用硬件加速

          Android 3.0版本后,Android 2D图形库能在大多数Android设备上使用GPU(图形处理单元)加速。GPU硬件
  加速可以极大的优化多数应用程序,但它并不是每个应用程序的最优选择。Android框架给予你是否在应用程序中
  使用硬件加速的控制力。

值得注意的是,我们必须手动在配置文件中设置应用程序API级别为11或者更高级别,即在 AndroidManifest.xml进行如下配置:
 

 <uses-sdk android:targetSdkVersion="11"/>

         一旦你开启了硬件加速,你可能看不到效率的提升。Mobile GPUs 善于处理特定的任务,例如:伸缩、旋转、
   平移图片。它也有一些不擅长处理的任务,例如:绘制直线或曲线。常言道物尽其用,扬长避短,尽可能让GPU
   处理它擅长的任务,减少让其处理弱势任务的。


         在PieChart 示例中,例如,相对来说绘制一个圆形是比较耗费资源的。每次旋转引起的重绘导致UI的迟缓。
   解决办法就是让View来呈现该圆形,并且设置该View的layer type属性为 LAYER_TYPE_HARDWARE,因此GPU
   能够缓存静态图片。示例中该View作为 PieChart类的内部类存在,减少了为了实现这个方法的代码开销。

private class PieView extends View { 
 
 public PieView(Context context) { 
  super(context); 
  if (!isInEditMode()) { 
   setLayerType(View.LAYER_TYPE_HARDWARE, null); 
  } 
 } 
  
 @Override 
 protected void onDraw(Canvas canvas) { 
  super.onDraw(canvas); 
 
  for (Item it : mData) { 
   mPiePaint.setShader(it.mShader); 
   canvas.drawArc(mBounds, 
     360 - it.mEndAngle, 
     it.mEndAngle - it.mStartAngle, 
     true, mPiePaint); 
  } 
 } 
 
 @Override 
 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
  mBounds = new RectF(0, 0, w, h); 
 } 
 
 RectF mBounds; 
} 


          改变之后,只有View第一次显示的时候才会调用PieChart.PieView.onDraw()方法。在应用程序的其他
   时间,绘制的图像将会作为图片缓存,重绘时GPU将任意旋转图像。


         然而这只是一个折中手段。缓存图片作为硬件层导致 video memory开销,video memory却是一种受限制的
 资源。 出于这个原因,在PieChart.PieView的最终版本上,只有在用户滑动时才设置它的layer type属性为
  LAYER_TYPE_HARDWARE。在其他时间,仅仅设置它的layer type属性为 LAYER_TYPE_HARDWARE,这
  允许GPU停止缓存图片。


        最后,不要忘记分析你的代码。在一个View上做的优化技术可能会在其他View上产生不好的影响。

共收到0条回复

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。