性能优化(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 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  GodEye install(Class> clz, T config) {
        getModule(clz).install(config);
        return this;
    }

我们看到这个方法还是比较简单的,首先第一个参数是要安装的模块类名,这个类是实现了Install接口的,第二个参数呢是要安装模块的相关配置,一般都放在**ContextImpl类中。然后我们看到方法里面调用了方法getModule,这个方法主要就是获取要实例化要安装的模块。

  public  T getModule(Class 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 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>() {
                            @Override
                            public ObservableSource apply(Long aLong) throws Exception {
                                return create();
                            }
                        }).subscribe(new Consumer() {
                    @Override
                    public void accept(FpsInfo fpsInfo) throws Exception {
                        mProducer.produce(fpsInfo);
                    }
                })
        );
    }

这个方法熟悉rxjava的人应该很熟悉了,这个就是创建了一个定时执行的发射器,主要我们看数据获取的地方是create()方法:

  private Observable create() {
        return Observable.create(new ObservableOnSubscribe() {
            @Override
            public void subscribe(final ObservableEmitter 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 implements Install {
}

我们看到这边继承了ProdeceableSubject类,如下所示:

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

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

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

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

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

我们看到这里基本就是Rxjava的知识了,PublishSubject与普通Subject不同,在订阅时并不立即触发订阅事件,而是允许我们在任意时刻手动调用onNext(),onError(),onCompleted来触发事件。所以这里我们就将产生的数据发送出去了。最终的消费是在android-godeye-monitor中,如下所示:

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

这样我们就获取到了数据产生中心的帧率信息了。当然框架里面还有显示数据的模块,这里我们重点还是放在帧率的获取上面,我们获取到帧率信息然后和系统的刷新频率进行对比,如果小于的话,那么说明有掉帧的现象,我们就可以记录下来。当然这只是一个指标,我们下面要讲一个更能精确记录页面卡顿情况的指标,就是SM(流畅度)。

总结:这篇只是粗略讲了帧率的获取,在实际生产环境中,我们希望通过各个指标的信息,怎么多维地分析出出现的问题,而且能准确定位错误的位置,这是我们的终极目标。

你可能感兴趣的:(性能优化(10)-AndroidGodEye解析之帧率(fps))