Android页面性能优化手册

写在前面的话

        一般情况下,只有当我们发现“严重”的性能问题时,我们才会开始着手进行性能优化。此时,虽然可以针对性的解决严重的性能问题,但在继续优化过程中,面对无数细小的“不良”代码,却又力不从心。所以,为了得到的微小的性能改善,庞大的工作量和复杂的历史逻辑却让人望而却步。不得不承认,无数细小的不良代码所累加的性能问题是不可忽视的。面对这样的问题,最佳的解决办法就是总结优化过程中发现的不良代码,从编码之初就保留对性能的“敬畏心”和sense,才能根本构建良好的性能优化良性循环。

        传统的编码规范仅只是增强代码的可读性和可维护性,本文的目的是整理提供常见的引起性能问题的代码习惯及对应规范,从编码源头硬性的对将会影响程序性能的操作进行规范,杜绝使用一定会引起性能问题的代码,以及给出更优的建议代码。总之,每个开发者都应该在开发过程中保持性能sense,尽可能高性能的完成产品功能需求。

1. 布局优化

1.1 布局层次扁平化

  • 控制布局层次,减少Activity上的布局层次,activty层的布局每多一层,页面上每个view的渲染都深一层,影响非常大!所以,请先检查所在页面的activity的页面层次是否做法“减无可减”。
  • 对于Adapter控件,如RecyclerView,item的布局层数也要严格控制,一般情况不要超过2层,card3.0由于使用了粒度更细的复用view,为了确保通用性,一布局层次比card2.0平均多了1层,card3.0的渲染性能较2.0略差。
  • 合理使用<include />、<merge />,保持布局扁平化,简单化,按需加载。

1.2 保证布局效率

  • 合理选择控件容器,如果可以达到相同的布局效果,尽量使用LinearLayout & FrameLayout 代替RelativeLayout。
  • 避免overDraw,将Acitivity 中的Window 的背景图设置为空,android的默认背景不为空,将Activity的背景放到Activity的Theme中设置,同时避免fragment和activity背景重复设置。

    设置Activity背景

    getWindow().setBackgroundDrawable(null);

    //Theme设置属性

    <item name="android:windowBackground">src_image</item>

  • 去掉不必要的view背景,如:card3.0针对白色背景的block,父层级row直接使用了透明背景。
  • 避免"overDesign",有道是“由俭入奢易,由奢入俭难",简单易用才是本质,过度设计可能反而会给用户带来不好的体验。
    举个例子,card3.0有“对普通文本支持左右image,即包装到一个LinearLayout”的设计,该设计会让block的文本区域布局层次多一层,为了10%的需求,牺牲90%的block同样性能折损。所以,针对这个情况,我们针对常用block舍弃了该设计,优化后,页面上下滑动更为流畅。
  • <ViewStub />标签,高效占位符,按需加载布局。ViewStub是一个轻量级的View,占用资源非常小的控件。我们可以为ViewStub指定一个布局,在Inflate布局的时候,ViewStub只会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。

    ViewStub

    <ViewStub

        android:id="@+id/stub_view"

        android:inflatedId="@+id/test_stub"

        android:layout="@layout/test_stub_layout"

        android:layout_width="fill_parent"

        android:layout_height="wrap_content"

        android:layout_gravity="bottom" />

    加载布局时,可以使用下面其中一种方法:

    ((ViewStub) findViewById(R.id.test_stub)).setVisibility(View.VISIBLE);

    View importPanel = ((ViewStub) findViewById(R.id.test_stub)).inflate();

  • 显而易见,使用代码实现布局代替xml文件,可以在一定程度上减少inflate的时间,缩短在一个doFrame周期页面渲染耗时,这也是我们在优化过程中的一个重要手段!

1.3 自定义控件原则

  • 避免在自定义布局的onDraw里创建对象;
  • 避免在复杂的布局上使用动画,如:底tab的动效影响底tab帧耗时10%左右!

2. RecylerView优化

         首先,如果你还在使用ListView,替换RecyclerView可能是更好的选择。比如,之前的版本基线由于短视频播放器上的部分实现问题,短视频所在页面使用ListView,切换RecyclerView之后,页面渲染性能大幅度提高。

2.1 共享缓存池RecycledViewPool

RecyclerView view1 = new RecyclerView(context);

LinearLayoutManager layout = new LinearLayoutManager(context);

layout.setRecycleChildrenOnDetach(true);

view1.setLayoutManager(layout);

RecycledViewPool pool = view1.getRecycledViewPool();

//...

RecyclerView view2 = new RecyclerView(context);

//... (set layout manager)

view2.setRecycledViewPool(pool);

//...

RecyclerView view3 = new RecyclerView(context);

//...(set layout manager)

view3.setRecycledViewPool(pool);

       合理使用recyclerView页面采用共享缓存池RecycledViewPool,对象池可以节省创建viewHolder的开销,更能避免GC,使用中要注意一下几点:

  • RecycledViewPool是依据Item的viewType来索引viewHolder的,所以你必须确保共享的RecyclerView的Adapter是同一个,或viewType是不会冲突的;
  • RecycledViewPool可以自主控制需要缓存的ViewHolder数量,并不是缓存越多越好,如下设置:  

    共享缓存池

    mPool.setMaxRecycledViews(itemViewType, number);

  • 在合适的时机,RecycledViewPool会自我清除掉所持有的ViewHolder对象引用,不用担心池子会“侧漏”。

2.2 科学合理的notify

 合理采用局部刷新,替代notifyDataSetChanged,当只需要针对某个位置刷新数据时,可以选用一下方法进行局部刷新

  • RecyclerView局部刷新

    //改变列表某个位置的item view

    notifyItemChanged(int position)

    //插入列表某个位置

    notifyItemInserted(int position)

    //删除列表某个位置

    notifyItemRemoved(int position)

    notifyItemMoved(int fromPosition, int toPosition)

    notifyItemRangeChanged(int positionStart, int itemCount)

    notifyItemRangeInserted(int positionStart, int itemCount)

    notifyItemRangeRemoved(int positionStart, int itemCount)

2.3 优化加载图片时机

       图片加载对于RecyclerView渲染是尤为关键的一步,对列表滑动的流畅性有很大的影响。所以,在列表滑动过程中暂停对图片的加载,当滑动停止时恢复加载,该优化可以大幅度提高用户的流畅体验值。需要注意的是,这样也会导致用户对于滑动过程中图片未正常加载产生疑惑,需要评估对用户体验的影响,也可专门针对低端机做此优化

  • 滑动暂停加载图片

    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

        @Override

        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {

            if (newState == RecyclerView.SCROLL_STATE_IDLE) {        

                ImageLoader.setPauseWork(false); //恢复加载

            else {    

                ImageLoader.setPauseWork(true); //暂停加载

            }

        }

    });

2.4 其他tips

  • 设置所有的 Item 的高度固定大小,这样可以减少item测量次数,尤其是对于 GridLayoutManager。

    mRecyclerView.setHasFixedSize(true);

  • 在onCreateViewHolder 和 onBindViewHolder 对时间敏感的方法中,尽量避免繁琐的操作和循环创建对象。

3. UI线程耗时操作优化

3.1 什么是UI线程

        Android应用的UI线程的概念及其重要性是每个Android开发者都应该理解的。当一个应用启动,系统会为应用创建一个名为“main”的主线程。这个主线程(也就是UI主线程)主要负责向UI组件分发事件(包括绘制事件),它也是你的应用和Android的UI组件交互的线程,因此它非常重要。例如,如果你点击了屏幕上的一个按钮,UI线程会把点击事件交给view处理,view接到事件后会设置它的pressed状态,然后向事件队列中发送一个invalidate请求,UI线程会依次读取请求队列并且通知view去重绘。

       当App在UI线程做一些比较繁重的操作的时候(比如网络请求、数据库操作等相关操作),就会阻塞UI线程,导致事件停止分发。对于用户来说,应用看起来就像卡住了,更坏的情况是,如果UI线程阻塞时间过长,用户就会看到应用无响应提示(ANR)。

所以,我们应该遵循两条基本原则 :

1. 不要阻塞UI线程;

2. 不要再UI线程外操作UI组件。

3.2 常见的耗时操作

  • 数据库操作,禁止在UI线程操作数据库!
  • 网络请求
  • 下载操作
  • 解析json数据,最好不要再UI线程进行解析json和转json的操作,这一点在优化过程中曾多次出现!
  • 复杂的计算逻辑,如高斯模糊算法等
  • IO操作,如读取文化、写文件等操作

4. 合理使用预加载机制

  • 当页面是ViewPager结构时,通常在进入页面时会默认预加载左右各一屏的页面,导致UI线程工作异常堵塞,在实际的业务场景下,取消不必要的预加载非常重要,我们可以根据需要衡量预加载左右tab的必要性。
    如:爱奇艺APP的vip tab又内外两层viewPager组成,启动APP后点击Vip tab在最好的情况下需要预加载并渲染4个页面(最差为6个页面,其中还包括部分RN页面,RN页面的初始化也较为耗时)的数据,给用户的感受就是点击后非常卡,我们做的优化就是:取消不必要的预加载,vip的渲染耗时提升为原来的35%左右!

  • 在APP的启动阶段,常常要做大量的初始化操作,但是我们需要遵循的原则就是:

    1、可以放到子线程执行的任务,不放到主线程。

    2、按需加载,科学评估预加载、初始化等操作的必要。

    3、分阶段解读任务,避免UI线程的工作挤在一个特定的时间窗口。

          在实际的优化工作中,由于在初始化阶段延时执行耗时任务导致启动后页面加载受影响的case也并不少见,而这些case基本都可以通过合适的异步操作或者延时操作来解决。

5.  小心细小但频繁的工作

      频繁但细小的工作是在优化过程中最容易忽略的部分,但是恰恰是最容易优化的部分,细小的工作优化“一小步”,你会发现页面整体的流畅度会前进“一大步”!下面就介绍一些我们对爱奇艺APP优化过的一些点:

  •  “我的”页面pingback的字段拼接在主线程,看起来每次拼接好像并不是很耗时,但是页面进来会发送一批展示pingback,总体上的影响还是很大的
  •  优化BarConfig工具类、ScreenTool工具类等,避免重复获取各种属性
  •  在没有线程安全的要求时,字符串拼接用StringBuilder代替StringBuffer和String(Card页面url拼接方式优化,效率提高约40%)
  •  对于频繁进行的SP存储,尽量不选择立即存储到文件(首页消息card存储优化)
  •  谨防内存泄露
  •  避免加载过大图片,压缩或者使用对象池后再使用,用Webp代替传统png图片
  •  避免在循环(for、while、getView方法、onDraw)里创建对象

  •  避免使用大量使用反射(EventBus优化,减少不必要的注册,杜绝无意义的反射)

结语

      页面性能优化是一项需要长期关注且重要的工作,页面的流畅与否直接影响到用户对我们产品的评价和褒贬。所以,只有我们开发者每个人都保持着对性能问题的高度警觉性,以良好的习惯和高效的实现面对每一行代码,我们的产品一定会越来越好。最后,感谢基线APP的各位同学在我们性能优化工作过程中的配合和协助!

猜你喜欢

转载自blog.csdn.net/cpcpcp123/article/details/121654139