文中所有源码基于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/cpp1.共享内存何时创建
在 中提到,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.cppstatic 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::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.cppvoid ProfileDataContainer::switchStorageToAshmem(int ashmemfd) { int regionSize = ashmem_get_size_region(ashmemfd); if (regionSize < static_cast (sizeof(ProfileData))) { reutrn; } // 1.创建ProfileData ProfileData* newData = reinterpret_cast( 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结构。在一文中提到,dump的数据是sizeof(ProfileData)的大小,这里就是答案了。 第二 : 之前猜测,为什么是数据switch to Ashmen,这个merge应该可以解释,之前的确是存在数据的。至此我们在java层创建的fd就跟底层的ProfileData绑定在一起了,数据是何时存储到里面的呢? 首先看在RenderThread类里创建的 ProfileDataContainer 这个变量,谁拿走去填充数据了呢? 追踪源码,可定位到:
CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory, std::unique_ptrrenderPipeline) : 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(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.cppvoid 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(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.cppstatic 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(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呈现模式分析 到底是什么,显示在屏幕上那些柱状图又是何时将什么数据绘制上的?