性能優化(10)-AndroidGodEye解析之幀率(fps)

主目錄見:Android高級進階知識(這是總目錄索引)
框架源碼:AndroidGodEye

?講了這么多篇的性能優化,我們應該對性能優化有了一定的了解,今天我們就來講講這個開源的性能監測框架,讓我們從應用的角度來分析性能問題。今天我們要講的就是第一篇幀率,我們知道流暢度的衡量指標主要有三個:

  • 1.幀率FPS
  • 2.丟幀SF(Skipped frame)
  • 3.流暢度SM(SMoothness)

這三個方面能整體反映出界面的卡頓問題,之前我們講了[卡頓優化實例解析],這里列舉了很多可能卡頓的原因,今天我們這篇文章將從數據得出頁面是不是出現了卡頓,雖然1s中VSync的60個Loop中不是每個都在做繪制的工作FPS比較低,但并不能代表這個時候程序不流暢(如我將App放在那不動實測FPS為1),所以FPS為1這個數并不能代表當前App在UI上界面不流暢。但是對于不斷做繪制的項目,這個指標會比較準確,比如游戲,輸入法應用等。

一.AndroidGodEye基本使用

首先還是老樣子,我們要來分析這個項目,我們要先來看看這個項目的用法:
1.添加依賴

dependencies {
  implementation 'cn.hikyson.godeye:godeye-core:VERSION_NAME'
  debugImplementation 'cn.hikyson.godeye:godeye-monitor:VERSION_NAME'
  releaseImplementation 'cn.hikyson.godeye:godeye-monitor-no-op:VERSION_NAME'
  implementation 'cn.hikyson.godeye:godeye-toolbox:VERSION_NAME'
}

其中的版本名你自己可以從github上面的release獲取。

2.安裝模塊

// before v1.7.0
// GodEye.instance().installAll(getApplication(),new CrashFileProvider(context))
// after v1.7.0 ,install one by one
GodEye.instance().install(Cpu.class, new CpuContextImpl())
                .install(Battery.class, new BatteryContextImpl(this))
                .install(Fps.class, new FpsContextImpl(this))
                .install(Heap.class, Long.valueOf(2000))
                .install(Pss.class, new PssContextImpl(this))
                .install(Ram.class, new RamContextImpl(this))
                .install(Sm.class, new SmContextImpl(this, 1000, 300, 800))
                .install(Traffic.class, new TrafficContextImpl())
                .install(Crash.class, new CrashFileProvider(this))
                .install(ThreadDump.class, new ThreadContextImpl())
                .install(DeadLock.class, new DeadLockContextImpl(GodEye.instance().getModule(ThreadDump.class).subject(), new DeadlockDefaultThreadFilter()))
                .install(Pageload.class, new PageloadContextImpl(this))
                .install(LeakDetector.class, new LeakContextImpl2(this, new PermissionRequest() {
                    @Override
                    public Observable<Boolean> dispatchRequest(Activity activity, String... permissions) {
                        return new RxPermissions(activity).request(permissions);
                    }
                }));

在應用中安裝模塊,這個GodEye類在模塊android-godeye中,這個模塊是數據產生中心。這里版本1.7.0之前是全部進行安裝。

3.可選功能
卸載模塊(不推薦這么做):

// before v1.7.0
// GodEye.instance().uninstallAll()
// after v1.7.0 ,uninstall one by one
GodEye.instance().getModule(Cpu.class).uninstall();

安裝完模塊,數據產生中心就會獲取數據,如果你要拿到產生的數據可以這么做:

// before v1.7.0
// GodEye.instance().cpu().subject().subscribe()
// after v1.7.0, get module by class
GodEye.instance().getModule(Cpu.class).subject().subscribe();

4.安裝debug監聽模塊,GodEyeMonitor是入口類
消費數據產生中心android-godeye的數據:

GodEyeMonitor.work(context)

停止:

GodEyeMonitor.shutDown()

5.最后就可以從瀏覽器看圖表數據了

1)要保證手機和你的pc在同一個網絡段,然后打開瀏覽器輸入:你的手機ip:端口號
2)或者你可以通過你的usb來使用,運行adb forward tcp:5390 tcp:5390,然后打開http://localhost:5390/就可以了。

默認的端口是5390,而且程序打印出了日志,例如:Open AndroidGodEye dashboard [ http://xxx.xxx.xxx.xxx:5390" ] in your browser...

好啦,怎么使用基本就是照搬github上面的,接下來我們根據基本使用來說說幀率源代碼是怎么獲取的。

二.幀率

首先我們從安裝模塊開始看源碼,首先看數據產生中心android-godeye,這里的GodEye類是入口,我們看他的install方法:

 public final <T> GodEye install(Class<? extends Install<T>> clz, T config) {
        getModule(clz).install(config);
        return this;
    }

我們看到這個方法還是比較簡單的,首先第一個參數是要安裝的模塊類名,這個類是實現了Install接口的,第二個參數呢是要安裝模塊的相關配置,一般都放在**ContextImpl類中。然后我們看到方法里面調用了方法getModule,這個方法主要就是獲取要實例化要安裝的模塊。

  public <T> T getModule(Class<T> clz) {
        Object module = mCachedModules.get(clz);
        if (module != null) {
            if (!clz.isInstance(module)) {
                throw new IllegalStateException(clz.getName() + " must be instance of " + String.valueOf(module));
            }
            return (T) module;
        }
        try {
            T createdModule;
            if (LeakDetector.class.equals(clz)) {
                createdModule = (T) LeakDetector.instance();
            } else if (Sm.class.equals(clz)) {
                createdModule = (T) Sm.instance();
            } else {
                createdModule = clz.newInstance();
            }
            mCachedModules.put(clz, createdModule);
            return createdModule;
        } catch (Throwable e) {
            throw new IllegalStateException("Can not create instance of " + clz.getName() + ", " + String.valueOf(e));
        }
    }

我們看到這里將實例化的類放在緩存mCachedModules中,首先會從緩存中獲取模塊,如果沒有獲取到則再實例化。得到實例化的類之后,就調用他的install方法,我們今天要講幀率(FPS),所以我們看到類fpsinstall方法中:

  @Override
    public synchronized void install(FpsContext config) {
        if (mFpsEngine != null) {
            L.d("fps already installed, ignore.");
            return;
        }
        mFpsEngine = new FpsEngine(config.context(), this, config.intervalMillis());
        mFpsEngine.work();
        L.d("fps installed.");
    }

我們看傳進來的參數,這個參數就是我們幀率類要用到的配置,我們外面傳進來的是實現了FpsContextFpsContextImpl類:

public class FpsContextImpl implements FpsContext {
    private Context mContext;

    public FpsContextImpl(Context context) {
        mContext = context.getApplicationContext();
    }

    @Override
    public Context context() {
        return mContext;
    }

    @Override
    public long intervalMillis() {
        return 2000;
    }
}

我們看到這個配置主要有兩個內容,一個是上下文對象context,和間隔時間,因為幀率是間隔地去獲取的。如圖所示:


性能監測指標

然后我們接著看Fps#install()方法,我們看到方法里面初始化了FpsEngine類,我們跟進構造函數看看:

  public FpsEngine(Context context, Producer<FpsInfo> producer, long intervalMillis) {
        mContext = context;
        mProducer = producer;
        mIntervalMillis = intervalMillis;
        mCompositeDisposable = new CompositeDisposable();
    }

方法就是對屬性的一些賦值,接著我們看FpsEngine#work()方法:

  @Override
    public void work() {
        mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()).
                        concatMap(new Function<Long, ObservableSource<FpsInfo>>() {
                            @Override
                            public ObservableSource<FpsInfo> apply(Long aLong) throws Exception {
                                return create();
                            }
                        }).subscribe(new Consumer<FpsInfo>() {
                    @Override
                    public void accept(FpsInfo fpsInfo) throws Exception {
                        mProducer.produce(fpsInfo);
                    }
                })
        );
    }

這個方法熟悉rxjava的人應該很熟悉了,這個就是創建了一個定時執行的發射器,主要我們看數據獲取的地方是create()方法:

  private Observable<FpsInfo> create() {
        return Observable.create(new ObservableOnSubscribe<FpsInfo>() {
            @Override
            public void subscribe(final ObservableEmitter<FpsInfo> e) throws Exception {
                ThreadUtil.ensureMainThread("fps");
                final float systemRate = getRefreshRate(mContext);
                final Choreographer choreographer = Choreographer.getInstance();
                choreographer.postFrameCallback(new Choreographer.FrameCallback() {
                    @Override
                    public void doFrame(long frameTimeNanos) {
                        final long startTimeNanos = frameTimeNanos;
                        choreographer.postFrameCallback(new Choreographer.FrameCallback() {
                            @Override
                            public void doFrame(long frameTimeNanos) {
                                long frameInterval = frameTimeNanos - startTimeNanos;//計算兩幀的時間間隔
                                float fps = (float) (1000000000 / frameInterval);
                                e.onNext(new FpsInfo((int)Math.min(fps, systemRate), (int)systemRate));
                                e.onComplete();
                            }
                        });
                    }
                });
            }
        });
    }

這個就是核心代碼了,Android 4.1引入了VSync機制可以通過其Loop來了解當前App最高繪制能力。Choreographer接收顯示系統的時間脈沖(垂直同步信號-VSync信號),在下一個frame渲染時控制執行這些操作,控制同步處理輸入(Input)動畫(Animation)繪制(Draw)三個UI操作。具體的源碼分析見:屬性動畫源碼分析(Choreographer"編舞者")

Choreographer中可以實現FrameCallback接口,然后實現里邊的doFrame方法,可以獲取到幀率等信息,通過Choreographer.getInstance().postFrameCallback(new MyFPSFrameCallback());把你的回調添加到Choreographer之中,那么在下一個frame被渲染的時候就會回調你的callback,執行你定義的doFrame操作,這時候你就可以獲取到這一幀的開始渲染時間并做一些自己想做的事情了。

到這里,我們的系統刷新頻率和實際刷新幀率就獲取得到了,我們調用onNext()方法發送出去,這個方法是rxjava里面的,然后我們看到在work()方法里面的accept()方法里使用mProducer#produce()進行接收,這個方法是接口Producer中的方法,類Fps實現了這個接口:

public class Fps extends ProduceableSubject<FpsInfo> implements Install<FpsContext> {
}

我們看到這邊繼承了ProdeceableSubject類,如下所示:

public class ProduceableSubject<T> implements SubjectSupport<T>, Producer<T> {
    private Subject<T> mSubject;

    public ProduceableSubject() {
        mSubject = createSubject();
    }

    protected Subject<T> createSubject() {
        return PublishSubject.create();
    }

    @Override
    public void produce(T data) {
        mSubject.onNext(data);
    }

    @Override
    public Observable<T> subject() {
        return mSubject;
    }
}

我們看到這里基本就是Rxjava的知識了,PublishSubject與普通Subject不同,在訂閱時并不立即觸發訂閱事件,而是允許我們在任意時刻手動調用onNext(),onError(),onCompleted來觸發事件。所以這里我們就將產生的數據發送出去了。最終的消費是在android-godeye-monitor中,如下所示:

mCompositeDisposable.add(godEye.getModule(Fps.class).subject().subscribe(new Consumer<FpsInfo>() {
            @Override
            public void accept(FpsInfo fpsInfo) throws Exception {
                mPipe.pushFpsInfo(fpsInfo);
            }
        }));

這樣我們就獲取到了數據產生中心的幀率信息了。當然框架里面還有顯示數據的模塊,這里我們重點還是放在幀率的獲取上面,我們獲取到幀率信息然后和系統的刷新頻率進行對比,如果小于的話,那么說明有掉幀的現象,我們就可以記錄下來。當然這只是一個指標,我們下面要講一個更能精確記錄頁面卡頓情況的指標,就是SM(流暢度)。

總結:這篇只是粗略講了幀率的獲取,在實際生產環境中,我們希望通過各個指標的信息,怎么多維地分析出出現的問題,而且能準確定位錯誤的位置,這是我們的終極目標。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容