Android应用优化之流畅度

前言

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

流畅度

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

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

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的画面更新。api

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的绘制对整一个绘制流程有一个基本的理解,下面咱们更好地探究如何流畅,为何卡顿。app

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

清晰理解上面咱们所述的概念后,接着去理解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. 引发内存抖动、内存泄漏
相关文章
相关标签/搜索