性能优化(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(流畅度)。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容