Android应用优化之流畅度

前言

对于现今市面上针对于用户交互的应用,都有使用列表去展示信息。列表对于用户来说是十分好的浏览、接收信息的一个控件。对于产品来说,列表流畅度的重要性就不言而喻了。而流畅度的好坏,对一个产品的基本体验和口碑有着极大的影响。然而Android手机与iPhone手机对比,第一点往往就是流畅度的问题,对于技术来说,我们的Google亲爹,不断对这个诟病进行优化,包括GPU硬件加速、将Dalvik虚拟机换成ART等等,我们的代码也不断从ListView换成RecyclerView。当我们沾沾自喜地说我们的产品也能像飘柔那样顺滑,我们的产品经理说出一个词“竞品对比”,来了一个JSON数据结构是三层数组嵌套,直观来说就是嵌套三层RecyclerView,懵逼脸?当然我们编码肯定不会这样做。好了回到我们的流畅度问题。

流畅度

对于流畅度,我们首先会重点说到FPS问题,导致流畅度不足。FPS是Frames Per Second,Frame(画面、帧),p就是Per(每),s就是Second(秒)。人类大脑与眼睛对一个画面的连贯性感知有一个边界值,譬如我们看电影会觉得画面很自然连贯,其帧率通常为 24fps。

但经过国内BAT专业测试团队研究,我们常说的FPS与流畅度的关系并不准确。此刻我们要先理解60FPS、16ms这俩名词。我们在其他的文章里总会看到的名词。那它究竟是什么意思呢?它们出于官方出的性能优化视频Android Performance Patterns: Why 60fps? 对其解释说:

While 60 frames per second is actually the sweet pot. Great, smooth motion without all the tricks. And most humans can’t perceive the benefits of going higher than this number.
60fps是最恰当的帧率,是用户能感知到的最流畅的帧率,而且人眼和大脑之间的协作无法感知到超过 60fps的画面更新。

Now, it’s worth noting that the human eye is very discerning when it comes to inconsistencies in these frame rates
但值得注意的是人眼却能够感受到刷新频率不一致带来的卡顿现象,比如一会60fps,一会30fps,这是能够被感受到的,即卡顿。

As an app developer, your goal is clear. Keep your app at 60 frames per second. that’s means you have got 16 milliseconds per frame to do all of your work.That is input, computing, network and rendering every frame to stay fluid for your users.
所以作为开发者,你必须要保证你的所有操作在16ms**(1000毫秒/60帧)**内完成包括输入、计算、网络、渲染等这些操作,才能保证应用使用过程的流畅性。

基础概念

Android应用程序显示原理是:手机屏幕显示的内容是通过Android系统的SurfaceFLinger类,把当前系统里所有进程需要显示的信息经过测量、布局和绘制后的Surface渲染合成一帧,然后交到屏幕进行显示。

FPS就是1s内SurfaceFLinger提交到屏幕的帧数。

  • SurfaceFLinger:Android系统服务,负责管理Android系统的帧缓冲区,即显示屏幕。
  • Surface:Android应用的每个窗口对应一个画布(Canvas),即Surface,可以理解为Android应用程序的一个窗口。

Android应用程序的显示重点有绘制、渲染。

上面所说的绘制指的是Android的绘制机制。我们要从View的创建View的测量View的布局View的绘制对整一个绘制流程有一个基本的理解,下面我们更好地探究如何流畅,为什么卡顿。

大多数用户感觉卡顿等性能的问题根源就是渲染性能(Render Performance)。此时要从VSync机制开始。VSync机制是Android4.1引入的是Vertical Synchronization(垂直同步)的缩写。我们可以把它看作是一种定时中断,其目的是为了改善android的流畅程度。

清晰理解上面我们所述的概念后,接着去理解VSync机制。下图是VSync机制下的绘制显示过程,从下图中看到CPU、GPU处理时间都很快,都是少于一个VSync间隔,也就是16ms,都能在16ms的VSync内display显示对应的内容。

这里写图片描述

上图是一个相当理想状态下的情况,但是当我们要完成一些酷炫、复杂的界面时,CPU、GPU处理时间会出现较慢的情况。就如下图所示的情况。

这里写图片描述

在上图我们看到Display有两个A、B,这里涉及到另外一个概念,在我们在绘制UI的时候,会采用一种称为“双缓冲”的技术。双缓冲意思是使用两个缓冲区(SharedBufferStack中),其中一个称为Front Buffer,另外一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。理想情况下,这样一个刷新会在16ms内完成(60FPS),上图就是描述的这样一个刷新过程(Display处理前Front Buffer,CPU、GPU处理Back Buffer。

从上图我们看到CPU、GPU的处理情况,已经大于一个VSync的间隔(16ms),我们看到在Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示,这就会让视觉产生不协调,达不到60FPS,于是出现了丢帧(Skipped Frame,SF)现象。

另外在上图第二个16ms时间段内,CPU无所事事,因为A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦过了VSYNC时间点,CPU就不能被触发以处理绘制工作了。

此时就有一种想法,如果有第三个Buffer存在,那CPU此时也可以利用起来了。那在Android4.1引入了Triple Buffer,所以当双Buffer不够用时Triple Buffer的丢帧情况如下图。

这里写图片描述

所以从上图可以看到,在第二个VSync,CPU是用了C Buffer绘图。虽然还是会多显示A帧一次,但后续显示就比较顺畅了。

可能有同学对上面的理解有点吃力,我们用通俗点的例子去描述一下。Vsync机制就像一台转速固定的发动机(60转/s),每一转都是处理一些UI的操作,但是不是每一转都有事情干,例如我们挂空挡的时候。而有时候因为一些阻力的原因,导致某一圈工作量过大,超过了16ms,那么这发动机这秒内就不是60转了,我们将这个转速称为流畅度。

获取流畅度的值

上面描述到对于VSync机制,我们是理解为一种定时中断,这个概念我们可以试着与Loop产生一种联系。在VSync机制中1s内Loop运行的次数。在这样的机制下,我们在每一次的Loop运行前,我们记录一下,就能获取的流畅度的相对情况。

而Android中有一个叫画图的打杂工————Choreographer对象。Google的官方API描述是,它用于协调animations、input以及drawing的时序,并且每个Looper公用一个Choreographer对象。Choreographer中文翻译过来是”舞蹈指挥”,字面上的意思就是优雅地指挥以上三个UI操作一起跳一支舞。

Choreographer的构造方法:

private Choreographer(Looper looper) {    
  mLooper = looper;    
  mHandler = new FrameHandler(looper);    
  mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;    
  mLastFrameTimeNanos = Long.MIN_VALUE;    
  mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());    
  mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];   
  for (int i = 0; i <= CALLBACK_LAST; i++) {        
   mCallbackQueues[i] = new CallbackQueue();    
  }
}

通过深入分析UI 上层事件处理核心机制 ChoreographerAndroid Choreographer 源码分析等文章参考理解,从上面Choreographer的构造方法我们理解到:

  1. Choreographer根据一个Looper来生成,Looper和线程是一对一的关系,因此对于每一条线程都有对应的一个Choreographer。
  2. 初始化FrameHandler。接收处理消息。
  3. 初始化FrameDisplayEventReceiver。FrameDisplayEventReceiver用来接收垂直同步脉冲,就是VSync信号,VSync信号是一个时间脉冲,一般为60HZ,用来控制系统同步操作。
  4. 初始化mLastFrameTimeNanos(标记上一个frame的渲染时间)以及mFrameIntervalNanos(帧率,fps,一般手机上为1s/60)。
  5. 初始化CallbackQueue,callback队列,将在下一帧开始渲染时回调。

然而Choreographer的主要工作在doFrame中,我们针对来看doFrame函数:

void doFrame(long frameTimeNanos, int frame) {    
  final long startNanos;    
  synchronized (mLock) {        
    if (!mFrameScheduled) { //判断是否有callback需要执行,mFrameScheduled会在postCallBack的时候置为true,一次frame执行时置为false       
      return; // no work to do        
    }
    \\\\打印跳frame时间        
    if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {            
      mDebugPrintNextFrameTimeDelta = false;            
      Log.d(TAG, "Frame time delta: "                    
              + ((frameTimeNanos - mLastFrameTimeNanos) *  0.000001f) + " ms");        
    }
    //设置当前frame的Vsync信号到来时间        
    long intendedFrameTimeNanos = frameTimeNanos;        
    startNanos = System.nanoTime();//实际开始执行当前frame的时间
    //时间差        
    final long jitterNanos = startNanos - frameTimeNanos;        
    if (jitterNanos >= mFrameIntervalNanos) {
      //时间差大于一个时钟周期,认为跳frame            
      final long skippedFrames = jitterNanos / mFrameIntervalNanos;
      //跳frame数大于默认值,打印警告信息,默认值为30            
      if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {                
         Log.i(TAG, "Skipped " + skippedFrames + " frames!  "                        
                    + "The application may be doing too much work on its main thread.");            
      }
      //计算实际开始当前frame与时钟信号的偏差值            
      final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; 
      //打印偏差及跳帧信息           
      if (DEBUG_JANK) {                
        Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "                        
                  + "which is more than the frame interval of "                        
                  + (mFrameIntervalNanos * 0.000001f) + " ms!  "                        
                  + "Skipping " + skippedFrames + " frames and setting frame "                        
                  + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");            
       }
       //修正偏差值,忽略偏差,为了后续更好地同步工作            
       frameTimeNanos = startNanos - lastFrameOffset;        
    }
    ···
}

我截取了其中一段关于绘制和丢帧处理和判断,后面的是回调CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL;对我们的讨论的目的过于深奥就不全部截取了。

我们利用Choreographer中的一个回调接口,FrameCallback。

    public interface FrameCallback {
        /**
         * Called when a new display frame is being rendered.
         * ···
         */
        public void doFrame(long frameTimeNanos);
    }

doFrame()的注释翻译意思是:当新的一帧被绘制的时候被调用。因此我们利用这个特性,可以统计两帧绘制的时间间隔。

主要流程如下:

1.实现Choreographer.FrameCallback接口;
2.在doFrame中统计两帧绘制的时间;
3.启动监测和处理数据;

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public class SMFrameCallback implements Choreographer.FrameCallback {
    private String TAG = "#SMFrameCallback";
    public static final float deviceRefreshRateMs = 16.6f;
    public static long lastFrameTimeNanos = 0;//纳秒为单位
    public static long currentFrameTimeNanos = 0;
    public static SMFrameCallback sInstance;

    public void start() {
        Choreographer.getInstance().postFrameCallback(SMFrameCallback.getInstance());
    }

    public static SMFrameCallback getInstance() {
        if (sInstance == null) {
            sInstance = new SMFrameCallback();
        }
        return sInstance;
    }

    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos == 0) {
            lastFrameTimeNanos = frameTimeNanos;
            Choreographer.getInstance().postFrameCallback(this);
            return;
        }
        currentFrameTimeNanos = frameTimeNanos;
        // 计算两次doFrame的时间间隔
        long value = (currentFrameTimeNanos - lastFrameTimeNanos) / 1000000;

        int skipFrameCount = skipFrameCount(lastFrameTimeNanos, currentFrameTimeNanos, deviceRefreshRateMs);

        Log.e(TAG, "两次绘制时间间隔value=" + value + "  frameTimeNanos=" + frameTimeNanos + "  currentFrameTimeNanos=" + currentFrameTimeNanos + "  skipFrameCount=" + skipFrameCount + "");

        lastFrameTimeNanos = currentFrameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }

    /**
     * 计算跳过多少帧
     */
    private int skipFrameCount(long start, long end, float devRefreshRate) {
        int count = 0;
        long diffNs = end - start;

        long diffMs = TimeUnit.MILLISECONDS.convert(diffNs, TimeUnit.MILLISECONDS);
        long dev = Math.round(devRefreshRate);
        if (diffMs > dev) {
            long skipCount = diffMs / dev;
            count = (int) skipCount;
        }
        return count;
    }
}

通过上述的工具类,我们在需要检测的Activity中调用启动代码即可。

 SMFrameCallback.getInstance().start();

一般情况下,我们会写在我们的BaseActivity或者Activitylifecyclecallbacks中去调用。

自定义MyActivityLifeCycle实现Application.ActivityLifecycleCallbacks。

public class MyActivityLifeCycle implements Application.ActivityLifecycleCallbacks {
    private Handler mHandler = new Handler(Looper.getMainLooper());
    private boolean mPaused = true;
    private Runnable mCheckForegroundRunnable;
    private boolean mForeground = false;
    private static MyActivityLifeCycle sInstance;
    //当前Activity的弱引用
    private WeakReference<Activity> mActivityReference;

    protected final String TAG = "#MyActivityLifeCycle";

    public static final int ACTIVITY_ON_RESUME = 0;
    public static final int ACTIVITY_ON_PAUSE = 1;

    private MyActivityLifeCycle() {
    }

    public static synchronized MyActivityLifeCycle getInstance() {
        if (sInstance == null) {
            sInstance = new MyActivityLifeCycle();
        }
        return sInstance;
    }

    public Activity getCurrentActivity() {
        if (mActivityReference != null) {
            return mActivityReference.get();
        }
        return null;
    }

    public boolean isForeground() {
        return mForeground;
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        mActivityReference = new WeakReference<>(activity);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {
        String activityName = activity.getClass().getName();
        notifyActivityChanged(activityName, ACTIVITY_ON_RESUME);
        mPaused = false;
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
            FrameSkipMonitor.getInstance().setActivityName(activityName);
            FrameSkipMonitor.getInstance().OnActivityResume();
            if (!mForeground) {
                FrameSkipMonitor.getInstance().start();
            }
        }
        mForeground = true;
        if (mCheckForegroundRunnable != null) {
            mHandler.removeCallbacks(mCheckForegroundRunnable);
        }
        mActivityReference = new WeakReference<Activity>(activity);
    }

    @Override
    public void onActivityPaused(Activity activity) {
        notifyActivityChanged(activity.getClass().getName(), ACTIVITY_ON_PAUSE);
        mPaused = true;
        if (mCheckForegroundRunnable != null) {
            mHandler.removeCallbacks(mCheckForegroundRunnable);
        }
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN)
            FrameSkipMonitor.getInstance().OnActivityPause();

        mHandler.postDelayed(mCheckForegroundRunnable = new Runnable() {
            @Override
            public void run() {
                if (mPaused && mForeground) {
                    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
                        FrameSkipMonitor.getInstance().report();
                    }
                    mForeground = false;
                }
            }
        }, 1000);
    }
}

然后在自定义的Application中调用。

public class MyApplication extends Application {

    ···

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(MyActivityLifeCycle.getInstance());
    }
    ···
}

除了上述的Choreographer帧率检测外,还有loop()打印日志等方法来对帧率进行统计监测。这里就不一一举例了。

总结:

根据了解Google文档,我们理解到Android4.1引入了VSync机制,通过其Loop来了解当前App最高绘制能力。

  • 固定每隔16.6ms执行一次;
  • 如果没有事件的时候,同样会运行一个Loop;
  • 这个Loop在1s之内运行了多少次,可以表示为当前App绘制最高能力,即Android App卡顿程度;
  • 如果一次Loop执行时间超过16.6ms,即出现了丢帧情况。

所以通过VSync机制来描述流畅度是一个连续的过程,而在APP静止某个界面时,流畅度很高,但FPS很低,流畅度更加客观地描述APP的卡顿情况。

通过一个漫长的理论分析,我们即将在下一篇对引起卡顿原因的代码实操。我们先预先认知一下以下几点引起卡顿的原因:

  1. 布局Layout过于复杂,无法在16ms内完成渲染;
  2. View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;
  3. View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;
  4. 人为在UI线程中做轻微耗时操作,导致UI线程卡顿;
  5. 同一时间动画执行的次数过多,导致CPU或GPU负载过重;
  6. 内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;
  7. 冗余资源及逻辑等导致加载和执行缓慢;
  8. 工作线程优先级未设置为Process.THREAD_PRIORITY_BACKGROUND导致后台线程抢占UI线程cpu时间片,阻塞渲染操作;
  9. 引起内存抖动、内存泄漏
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页