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這樣的方法,然而它<b>重寫了操作符-></b>所以真正的實現還是在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呈現模式分析 到底是什么,顯示在屏幕上那些柱狀圖又是何時將什么數據繪制上的?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,610評論 25 708
  • 轉載請注明出處(http://www.lxweimin.com/p/5f538820e370),您的打賞是小編繼續...
    福later閱讀 27,612評論 8 72
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標準的Java I...
    JackChen1024閱讀 7,562評論 1 143
  • 蘭心咖啡廳里燈光暖暖的,柔和的光線與若有若無的音樂融和一起,令人心頭無端升起柔情蜜意。尤其看見一對對情侶或含情低語...
    泠風思語閱讀 460評論 0 1
  • 曾經,我認為,承擔就意味著成熟。但現在,才真真正正的明白!成熟是在明白這個世界的風風雨雨之后,還能客觀的去對待...
    36f262228c3d閱讀 22評論 0 0