文中所有源码基于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呈现模式分析 到底是什么,显示在屏幕上那些柱状图又是何时将什么数据绘制上的?