GraphicsStatsService之2 UI绘制的时间信息来源

文中所有源码基于Android8.0

用到的类:

GraphicsStatsService.java
ThreadedRenderer.java
android_view_ThreadedRenderer.cpp
RenderProxy.cpp
RenderTask.h/cpp
RenderThread.h/cpp
ProfileDataContainer.h/cpp
ConvasContext.h/cpp
JankTracker.h/cpp

1.共享内存何时创建

GraphicsStatsService-dump数据中提到,dump数据都是从ActiveBuffer这个类中的到的,其中包含了一个MemoryFile,下面是它的创建:


private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid,
            String packageName, int versionCode) throws RemoteException {
    int size = mActive.size();
    //1. 根据时间来判断,今天有没有创建buffer,如果有直接返回buffer
    long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis();
    for (int i = 0; i < size; i++) {
        ActiveBuffer buffer = mActive.get(i);
        if (buffer.mPid == pid
                && buffer.mUid == uid) {
            // If the buffer is too old we remove it and return a new one
            if (buffer.mInfo.startTime < today) {
                buffer.binderDied();
                break;
            } else {
                return buffer;
            }
        }
    }
    // 2.没找到buffer,创建一个
    try {
        ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode);
        mActive.add(buffers);
        return buffers;
    } catch (IOException ex) {
        throw new RemoteException("Failed to allocate space");
    }
}
复制代码

1.1 如果今天有合适的buffer,则不创建直接使用。超过了一天,则走它的进程死亡后的处理逻辑,然后走第2步,创建buffer。 从它的定时器也可以看出:


private Calendar normalizeDate(long timestamp) {
    Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
    calendar.setTimeInMillis(timestamp);
    //每天的0点
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    return calendar;
}

private void scheduleRotateLocked() {
    mRotateIsScheduled = true;
    Calendar calendar = normalizeDate(System.currentTimeMillis()); 
    //又添加了每个月的第一天
    calendar.add(Calendar.DATE, 1);
    mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm,
            mWriteOutHandler);
}
复制代码

每天的0点触发,证明有效期只有一天。

1.2 创建buffer,内部创建MemoryFile。

2. 如何与进程关联

共享内存创建后,要靠它的fd来读写数据,那么这个fd是怎么传出去的呢?


private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token,
            int uid, int pid, String packageName, int versionCode) throws RemoteException {
        ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode);
        scheduleRotateLocked();
        return getPfd(buffer.mProcessBuffer);
    }
private ParcelFileDescriptor getPfd(MemoryFile file) {
        try {
            ...
            return new ParcelFileDescriptor(file.getFileDescriptor());
        } catch (IOException ex) {
            ...
        }
    }
复制代码

包装成了一个ParcelFileDescriptor,由此可以看出,要进行Binder传输了,那么调用requestBufferForProcessLocked方法的地方,就是要创建它的地方。requestBufferForProcessLocked这个方法是IGraphicsStats.aidl生成的,说明要跨进程了,从它的名字上也可以看出端倪,为进程创建buffer。 追踪代码可以发现,在ThreadedRenderer.java中,其内部类 ProcessInitializer的init方法中进行调用了,而ThreadedRenderer是在ViewRootImpl.java中创建的,所以在创建Window时就会创建这个Buffer,如果这个进程已经有这个buffer了,则直接返回此buffer。


private void initGraphicsStats() {
    try {
        // 1. request buffer
        IBinder binder = ServiceManager.getService("graphicsstats");
        mGraphicsStatsService = IGraphicsStats.Stub.asInterface(binder);
        requestBuffer();
    } catch (Throwable t) {
    }
}

private void requestBuffer() {
    try {
        final String pkg = mAppContext.getApplicationInfo().packageName;
        // 2. 调用service的requestBufferForProcess方法
        ParcelFileDescriptor pfd = mGraphicsStatsService
                .requestBufferForProcess(pkg, mGraphicsStatsCallback);
        nSetProcessStatsBuffer(pfd.getFd());
        pfd.close();
    } catch (Throwable t) {
        Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t);
    }
}
复制代码

2.1 得到service
2.2 调用请求buffer的方法,然后通过一个native方法将fd设置到了底层,最后将fd关闭了。为什么给关了呢,不是要往里写数据吗?接着往下看:

# android_view_ThreadedRenderer.cpp
static void android_view_ThreadedRenderer_setProcessStatsBuffer(JNIEnv* env, jobject clazz,
        jint fd) {
    RenderProxy::setProcessStatsBuffer(fd);
}
复制代码

直接调用了RenderProxy的方法,setProcessStatsBuffer。

CREATE_BRIDGE2(setProcessStatsBuffer, RenderThread* thread, int fd) {
    args->thread->globalProfileData().switchStorageToAshmem(args->fd);
    close(args->fd);
    return nullptr;
}

void RenderProxy::setProcessStatsBuffer(int fd) {
    SETUP_TASK(setProcessStatsBuffer);
    auto& rt = RenderThread::getInstance();
    args->thread = &rt;
    args->fd = dup(fd);
    rt.queue(task);
}
复制代码

哦,执行了SETUP_TASK(setProcessStatsBuffer)这样一句,接着queue到RenderThread里面了。还有个CREATE_BRIDGE2(setProcessStatsBuffer, RenderThread* thread, int fd),这个是什么呢?
首先看CREATE_BRIDGE2,这是一个宏,如下:

// 将method和Args连接在一起
#define ARGS(method) method ## Args

#define CREATE_BRIDGE0(name) CREATE_BRIDGE(name,,,,,,,,)
#define CREATE_BRIDGE1(name, a1) CREATE_BRIDGE(name, a1,,,,,,,)
#define CREATE_BRIDGE2(name, a1, a2) CREATE_BRIDGE(name, a1,a2,,,,,,)
...其他参数个数的宏
#define CREATE_BRIDGE(name, a1, a2, a3, a4, a5, a6, a7, a8) \
    typedef struct { \
        a1; a2; a3; a4; a5; a6; a7; a8; \
    } ARGS(name); \
    static_assert(std::is_trivially_destructible<ARGS(name)>::value, \
            "Error, ARGS must be trivially destructible!"); \
    static void* Bridge_ ## name(ARGS(name)* args)

复制代码

那么,我们的参数是:
name : setProcessStatsBuffer
a1 : RenderThread* thread
a2 : int fd
将我们的代入看下是什么样子呢?

typedef struct {
	RenderThread* thread;
    int fd;
} setProcessStatsBufferArgs;

//省略assert这句

static void* Bridge_setProcessStatsBuffer(setProcessStatsBufferArgs* args) {
    args->thread->globalProfileData().switchStorageToAshmem(args->fd);
    close(args->fd);
    return nullptr;
}

复制代码

原来是声明了一个函数,那么SETUP_TASK想必就是调用它了。

#define SETUP_TASK(method) \
    ...省略判断
    MethodInvokeRenderTask* task = new MethodInvokeRenderTask((RunnableMethod) Bridge_ ## method); \
    ARGS(method) *args = (ARGS(method) *) task->payload()

复制代码

将我们的方法替换后得到下面的语句:

//定义函数指针
typedef void* (*RunnableMethod)(void* data);

void RenderProxy::setProcessStatsBuffer(int fd) {
    MethodInvokeRenderTask* task = 
    new MethodInvokeRenderTask((RunnableMethod)Bridge_setProcessStatsBuffer);
	setProcessStatsBufferArgs*args = (setProcessStatsBufferArgs*) task->payload()
    
    auto& rt = RenderThread::getInstance();
    args->thread = &rt;
    args->fd = dup(fd);
    rt.queue(task);
}

复制代码

将Bridge_setProcessStatsBuffer传给了MethodInvokeRenderTask,现在看下它的实现:

// Renderask.h

#define METHOD_INVOKE_PAYLOAD_SIZE (8 * sizeof(void*))

class MethodInvokeRenderTask : public RenderTask {
public:
    explicit MethodInvokeRenderTask(RunnableMethod method)
        : mMethod(method), mReturnPtr(nullptr) {}
	//1.返回了mData变量
    void* payload() { return mData; }
    void setReturnPtr(void** retptr) { mReturnPtr = retptr; }
	
    //2.执行了传进来的方法
    virtual void run() override {
        void* retval = mMethod(mData);
        if (mReturnPtr) {
            *mReturnPtr = retval;
        }
        // Commit suicide
        delete this;
    }
private:
    RunnableMethod mMethod;
    char mData[METHOD_INVOKE_PAYLOAD_SIZE];
    void** mReturnPtr;
};
复制代码

这里有两点
第一:payload()方法将mData返回给外面,并且在我们这个方法中强转成了setProcessStatsBufferArgs*,为什么就转换了呢? 我们看mData的size是METHOD_INVOKE_PAYLOAD_SIZE,也就是8个sizeof(void*)的大小,可以理解为8个sizeof(int*)的大小,64位的机子上就是8*8 = 64。
为什么是8呢?因为CREATE_BRIDGE这个宏最多支持8个参数。
第二:将参数传给Bridge_setProcessStatsBuffer,然后执行。

OK,现在回到我们的void RenderProxy::setProcessStatsBuffer(int fd)方法,将setProcessStatsBufferArgs*args填充成如下:

void RenderProxy::setProcessStatsBuffer(int fd) {
	....
   
    auto& rt = RenderThread::getInstance();
    1 线程填充为 RenderThread::getInstance
    args->thread = &rt;
    2 复制了一个fd
    args->fd = dup(fd);
    rt.queue(task);
}
复制代码

原来是将这个task放到了RenderThread中去执行了,fd用dup系统调用复制了一个,这就理解了java层为何直接close掉了。

在RenderThread类中,将这个task执行,也就是我们的函数执行:

static void* Bridge_setProcessStatsBuffer(setProcessStatsBufferArgs* args) {
	// 1 switch
    args->thread->globalProfileData().switchStorageToAshmem(args->fd);
    close(args->fd);
    return nullptr;
}
复制代码

原来是RenderThread里面拿到globalProfileData(),是ProfileDataContainer的变量,然后执行switchStorageToAshmem(args->fd)。 这个函数的意思可以理解一下,switch to , 也就是说ProfileDataContainer这个变量可能一直有数据,现在将它的存储调整到了java曾创建的那个共享内存中。现在看下这个方法的实现:

// ProfileDataContainer.cpp
void ProfileDataContainer::switchStorageToAshmem(int ashmemfd) {
    int regionSize = ashmem_get_size_region(ashmemfd);
    if (regionSize < static_cast<int>(sizeof(ProfileData))) {
    	reutrn;
    }
    // 1.创建ProfileData
    ProfileData* newData = reinterpret_cast<ProfileData*>(
            mmap(NULL, sizeof(ProfileData), PROT_READ | PROT_WRITE,
                    MAP_SHARED, ashmemfd, 0));
    if (newData == MAP_FAILED) {
        int err = errno;
        ALOGW("Failed to move profile data to ashmem fd %d, error = %d",
                ashmemfd, err);
        return;
    }
	
    // 2. mergedata
    newData->mergeWith(*mData);
    freeData();
    mData = newData;
    mIsMapped = true;
}

复制代码

关键有两点
第一 : map一块内存,然后创建ProfileData结构。在GraphicsStatsService之1-dump数据一文中提到,dump的数据是sizeof(ProfileData)的大小,这里就是答案了。
第二 : 之前猜测,为什么是数据switch to Ashmen,这个merge应该可以解释,之前的确是存在数据的。

至此我们在java层创建的fd就跟底层的ProfileData绑定在一起了,数据是何时存储到里面的呢? 首先看在RenderThread类里创建的 ProfileDataContainer 这个变量,谁拿走去填充数据了呢? 追踪源码,可定位到:

CanvasContext::CanvasContext(RenderThread& thread, bool translucent,
        RenderNode* rootRenderNode, IContextFactory* contextFactory,
        std::unique_ptr<IRenderPipeline> renderPipeline)
        : mRenderThread(thread)
        , mOpaque(!translucent)
        , mAnimationContext(contextFactory->createAnimationContext(mRenderThread.timeLord()))
        , mJankTracker(&thread.globalProfileData(), thread.mainDisplayInfo())
        , mProfiler(mJankTracker.frames())
        , mContentDrawBounds(0, 0, 0, 0)
        , mRenderPipeline(std::move(renderPipeline)) {
...
}
复制代码

可以看到,原来是JankTracker创建时,拿走了它的引用。那么接着看JankTracker这个类的构造函数:

JankTracker::JankTracker(ProfileDataContainer* globalData, const DisplayInfo& displayInfo) {
    mGlobalData = globalData;
    nsecs_t frameIntervalNanos = static_cast<nsecs_t>(1_s / displayInfo.fps);
    setFrameInterval(frameIntervalNanos);
}

复制代码

哦,原来是付给自己的成员变量,那么它是什么时侯将数据写入的呢?
找到下面的函数:

void JankTracker::finishFrame(const FrameInfo& frame) {
    // Fast-path for jank-free frames
    int64_t totalDuration = frame.duration(sFrameStart, FrameInfoIndex::FrameCompleted);
    ...先省略
	
    //1 记录绘制时间
    mData->reportFrame(totalDuration);
    (*mGlobalData)->reportFrame(totalDuration);
	
    //2 这一帧绘制时间正常
    // Keep the fast path as fast as possible.
    if (CC_LIKELY(totalDuration < mFrameInterval)) {
        return;
    }
	
    //3 有跳帧
    mData->reportJank();
    (*mGlobalData)->reportJank();
	
    
    for (int i = 0; i < NUM_BUCKETS; i++) {
        int64_t delta = frame.duration(COMPARISONS[i].start, COMPARISONS[i].end);
        if (delta >= mThresholds[i] && delta < IGNORE_EXCEEDING) {
            mData->reportJankType((JankType) i);
            (*mGlobalData)->reportJankType((JankType) i);
        }
    }
}

复制代码

我们关注 mGlobalData 这个变量,刚才是这个变量接收的,这个函数主要有三点
第一 : 记录绘制时间,然后存储在mGlobalData中,即ProfileDataContainer这个结构里。我们看下这个结构:

class ProfileDataContainer {
...
public:
	...
    void switchStorageToAshmem(int ashmemfd);
    ProfileData* get() { return mData; }
    ProfileData* operator->() { return mData; }

private:
    void freeData();
    ProfileData* mData = new ProfileData;
    bool mIsMapped = false;
};
复制代码

发现它并无reportFrame这样的方法,然而它重写了操作符->所以真正的实现还是在ProfileData这个结构里:

// ProfileData.cpp
void ProfileData::reportFrame(int64_t duration) {
    mTotalFrameCount++;
    uint32_t framebucket = frameCountIndexForFrameTime(duration);
    if (framebucket <= mFrameCounts.size()) {
        mFrameCounts[framebucket]++;
    } else {
        framebucket = (ns2ms(duration) - kSlowFrameBucketStartMs) / kSlowFrameBucketIntervalMs;
        framebucket = std::min(framebucket, static_cast<uint32_t>(mSlowFrameCounts.size() - 1));
        mSlowFrameCounts[framebucket]++;
    }
}

复制代码

终于,原来数据的记录在这,这个方法记录着总帧数和哪个柱状图的数据。

第二 : 当绘制一帧的时间小于mFrameInterval,就直接返回了,mFrameInterval的值一般是 1/60 ms,也就是平时说的16ms。
第三 : 当一帧的时间大于正常值,就属于Jank了,那么就按jank记录下来。同第一步的分析。

到此,我们看到了数据是怎么存储的,那么是什么时侯调用这个存储方法呢?

继续追踪源码,发现CanvasContext的draw()方法,每绘制一帧,就调用一下:

void CanvasContext::draw() {
	...省略
    bool didSwap = mRenderPipeline->swapBuffers(frame, drew, windowDirty, mCurrentFrameInfo,
            &requireSwap);

    mIsDirty = false;

	...省略

    mJankTracker.finishFrame(*mCurrentFrameInfo);


}
复制代码

最后,可以看出每绘制一帧ui,都要记录一下,这一帧用了多久,是否jank了,原因是什么等数据。数据的写入流程分析完成。

3.数据存哪里了

前面说道,java层有定时器并且还有binder死亡的监听,然后保存数据到本地. 从进程死亡入手,毕竟杀app是很正常的事: 在GraphicsStatsService.java中,ActiveBuffer中有一个IGraphicsStatsCallback参数,是app进程的里binder对象, 然后监听了它的死亡,当它死亡时,会走binderDied回调方法,进一步处理后,调用

private static native void nSaveBuffer(String path, String packageName, int versionCode,
            long startTime, long endTime, byte[] data);
复制代码

参数path是什么呢?拼接字符串可以看出是 /data/system/graphicsstats/时间/包名/版本号/total这样一个路径,比如 在手机上看到: /data/system/graphicsstats/1531440000000/com.sdu.didi.psnger,然后继续往下看:

// com_android_server_GraphicsStatsService.cpp
static void saveBuffer(JNIEnv* env, jobject clazz, jstring jpath, jstring jpackage,
        jint versionCode, jlong startTime, jlong endTime, jbyteArray jdata) {
    ScopedByteArrayRO buffer(env, jdata);
	...省略
    const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer.get());
    GraphicsStatsService::saveBuffer(path, package, versionCode, startTime, endTime, data);
}
复制代码

可以看数据转换成ProfileData后,直接调用了GraphicsStatsService.cpp的方法:

void GraphicsStatsService::saveBuffer(const std::string& path, const std::string& package,
        int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
    service::GraphicsStatsProto statsProto;
    //1 节写之前存在的,并与要写入的合并
    if (!parseFromFile(path, &statsProto)) {
        statsProto.Clear();
    }
    if (!mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data)) {
        return;
    }
 
 	//2 按protobuf写入
    int outFd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0660);
    int wrote = write(outFd, &sCurrentFileVersion, sHeaderSize);
 
    {
        FileOutputStreamLite output(outFd);
        bool success = statsProto.SerializeToZeroCopyStream(&output) && output.Flush();
 
    close(outFd);
}

复制代码

主要有两点,从文件里按protobuf解析出来,然后在合并,然后将新数据写回去。

注:protobuf 是google的数据序列化格式,主要优点是轻便,高效。用protobuf的语言描述后,可以用工具直接转换成c++,java,python等的接口,很方便使用

下一节讨论 设置中的 GPU呈现模式分析 到底是什么,显示在屏幕上那些柱状图又是何时将什么数据绘制上的?

转载于:https://juejin.im/post/5b49bd23f265da0f87591efc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值