深入淺出Android屏幕刷新原理

如需轉(zhuǎn)載請?jiān)u論或簡信啄骇,并注明出處痴鳄,未經(jīng)允許不得轉(zhuǎn)載

目錄

前言

現(xiàn)在Android的應(yīng)用界面越來越復(fù)雜,很多時候頁面中還有各種動畫缸夹,所以頁面卡頓痪寻、掉幀等問題就隨之而來,所以就想研究一下屏幕刷新的原理虽惭,以便于更快的定位和解決問題

基本概念

Android的屏幕刷新中涉及到最重要的三個概念(為便于理解橡类,這里先做簡單介紹)

CPU:執(zhí)行應(yīng)用層的measure、layout芽唇、draw等操作顾画,繪制完成后將數(shù)據(jù)提交給GPU

GPU:進(jìn)一步處理數(shù)據(jù),并將數(shù)據(jù)緩存起來

屏幕:由一個個像素點(diǎn)組成匆笤,以固定的頻率(16.6ms研侣,即1秒60幀)從緩沖區(qū)中取出數(shù)據(jù)來填充像素點(diǎn)

總結(jié)一句話就是:CPU 繪制后提交數(shù)據(jù)、GPU 進(jìn)一步處理和緩存數(shù)據(jù)炮捧、最后屏幕從緩沖區(qū)中讀取數(shù)據(jù)并顯示

我們開發(fā)過程中主要關(guān)心CPU繪制部分庶诡,對GPU和屏幕基本不用關(guān)心。所以咆课,看到這里末誓,有的人可能就會想說扯俱,我對view的繪制流程(measure、layout基显、draw)已經(jīng)非常熟悉蘸吓,至于GPU和屏幕,和我也沒有太大關(guān)系吧撩幽。其實(shí)這里面還有更多的細(xì)節(jié)值得我們?nèi)ヌ剿骺饧蹋私夂驼莆樟诉@些細(xì)節(jié),有助于我們解決一些實(shí)際開發(fā)過程中的問題窜醉,我們不妨一步步往下看

雙緩沖機(jī)制

看完上面的流程圖宪萄,我們很容易想到一個問題,屏幕是以16.6ms的固定頻率進(jìn)行刷新的榨惰,但是我們應(yīng)用層觸發(fā)繪制的時機(jī)是完全隨機(jī)的(比如我們隨時都可以觸摸屏幕觸發(fā)繪制)拜英,如果在GPU向緩沖區(qū)寫入數(shù)據(jù)的同時,屏幕也在向緩沖區(qū)讀取數(shù)據(jù)琅催,會發(fā)生什么情況呢居凶?有可能屏幕上就會出現(xiàn)一部分是前一幀的畫面,一部分是另一幀的畫面藤抡,這顯然是無法接受的侠碧,那怎么解決這個問題呢?

這個其實(shí)和我們平時使用代碼管理工具Git的一些思路有相似之處缠黍,首先我們有一個master分支弄兜,對應(yīng)線上版本的代碼,當(dāng)有新的需求來的時候瓷式,我們往往不會在master分支上直接進(jìn)行開發(fā)替饿,都會拉出一個新的分支,比如develop分支贸典,在develop分支上開發(fā)新需求视卢,等開發(fā)完成測試通過后才會合并到master分支

所以,在屏幕刷新中廊驼,Android系統(tǒng)引入了雙緩沖機(jī)制腾夯。GPU只向Back Buffer中寫入繪制數(shù)據(jù),且GPU會定期交換Back Buffer和Frame Buffer蔬充,也就是讓Back Buffer 變成Frame Buffer交給屏幕進(jìn)行繪制蝶俱,讓原先的Frame Buffer變成Back Buffer進(jìn)行數(shù)據(jù)寫入。交換的頻率也是60次/秒饥漫,這就與屏幕的刷新頻率保持了同步

雖然我們引入了雙緩沖機(jī)制榨呆,但是我們知道,當(dāng)布局比較復(fù)雜庸队,或設(shè)備性能較差的時候积蜻,CPU并不能保證在16.6ms內(nèi)就完成繪制數(shù)據(jù)的計(jì)算闯割,所以這里系統(tǒng)又做了一個處理

當(dāng)你的應(yīng)用正在往Back Buffer中填充數(shù)據(jù)時,系統(tǒng)會將Back Buffer鎖定竿拆。如果到了GPU交換兩個Buffer的時間點(diǎn)宙拉,你的應(yīng)用還在往Back Buffer中填充數(shù)據(jù),GPU會發(fā)現(xiàn)Back Buffer被鎖定了丙笋,它會放棄這次交換

這樣做的后果就是手機(jī)屏幕仍然顯示原先的圖像谢澈,這就是我們常常說的丟幀,所以為了避免丟幀的發(fā)生御板,我們就要盡量減少布局層級锥忿,減少不必要的View的invalidate調(diào)用,減少大量對象的創(chuàng)建(GC也會占用CPU時間)等等怠肋。對這方面有興趣的可以看我的性能優(yōu)化專題下的文章

Choreographer

我們看下面這張圖敬鬓,這里已經(jīng)是基于雙緩沖機(jī)制,且應(yīng)用層的優(yōu)化已經(jīng)做得非常好笙各,繪制時間均少于16.6ms钉答,但依然出現(xiàn)了丟幀,為什么呢杈抢?

原因是第2幀雖然繪制時間少于16.6ms希痴,但是繪制開始的時間距離vsync信號(就是一個發(fā)起屏幕刷新的信號,Vertical Synchronization的縮寫)發(fā)出的時間比較短暫春感,導(dǎo)致當(dāng)vsync信號來的時候,第2幀還沒有繪制完成虏缸,所以Back Buffer依然是鎖定的狀態(tài)鲫懒,也就出現(xiàn)了丟幀

如果我們可以保證每次繪制開始的時間和vsync信號發(fā)起的時間一致(如下圖所示),是不是就可以解決這個問題呢刽辙?

Android在每一幀中實(shí)際上只是在完成三個操作窥岩,分別是輸入(Input)動畫(Animation)宰缤、繪制(Draw)颂翼。在Android4.1(API 16)之后,Android系統(tǒng)開始加入Choreographer這個類慨灭,這個類名翻譯過來是“舞蹈指導(dǎo)”朦乏,字面上的意思就是指揮以上三個UI操作一起完成一支舞蹈。這個類就可以解決vsync和繪制不同步的問題氧骤,其實(shí)它的原理用一句話總結(jié)就是往Choreographer里發(fā)一個消息呻疹,最快也要等到下一個vsync信號來的時候才會開始處理消息

下面我們通過源碼分析來看看Choreographer的實(shí)現(xiàn)原理

Activity中的布局首次繪制,以及每次調(diào)用View 的 invalidate() 時筹陵,都會調(diào)用到ViewRootImp#requestLayout()刽锤,對于這塊不是很清楚的具體可以看最全的View繪制流程(上)— Window镊尺、DecorView、ViewRootImp的關(guān)系并思,所以我們接下來分析一下ViewRootImp#requestLayout()里面做了什么

ViewRootImp#requestLayout()

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //檢查是否是主線程庐氮,不然會拋出異常
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

ViewRootImp#scheduleTraversals()

如果在同一幀中出現(xiàn)多次requestLayout()調(diào)用,其實(shí)最終也只會繪制一次宋彼,為什么呢弄砍?我們可以看到下面有個mTraversalScheduled標(biāo)志位,稍后我們可以看看這個標(biāo)志位是哪里被置為false的

void scheduleTraversals() {
    if (!mTraversalScheduled) {         
        mTraversalScheduled = true;
        //添加同步消息屏障宙暇,這個方法也比較關(guān)鍵输枯,這里先不關(guān)心,我們說完Choreographer再分析
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //向Choreographer中發(fā)送消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //...
    }
}

Choreographer#postCallbackDelayedInternal()

mChoreographer.postCallback()接著會調(diào)用這個方法

  private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //將消息以當(dāng)前的時間戳放進(jìn)mCallbackQueue 隊(duì)列里
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                //如果沒有設(shè)置消息延時占贫,直接執(zhí)行
                scheduleFrameLocked(now);
            } else {
                //消息延時桃熄,但是最終依然會調(diào)用scheduleFrameLocked
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

Choreographer#scheduleFrameLocked()

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            if (isRunningOnLooperThreadLocked()) {
                //如果當(dāng)前線程是Choreographer的工作線程,我理解就是主線程
                scheduleVsyncLocked();
            } else {
                //否則發(fā)一條消息到主線程
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                //設(shè)置消息為異步消息型奥,其實(shí)就是一個標(biāo)志位瞳收,具體作用我們后面會講
                msg.setAsynchronous(true);
                //插到消息隊(duì)列頭部,可以理解為設(shè)置最高優(yōu)先級
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
                }
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
    }
}

接下來最終會調(diào)用到一個native方法

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}
public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);

native方法我們在Android Studio中不能直接查看厢汹,這里我們換一種思路螟深。前面Choreographer#postCallbackDelayedInternal()方法中,我們看到了將消息以當(dāng)前的時間戳放進(jìn)隊(duì)列里烫葬,那消息什么時候被取出來執(zhí)行呢界弧?

CallbackQueue

private final class CallbackQueue {
    private CallbackRecord mHead;

    public boolean hasDueCallbacksLocked(long now) {
        return mHead != null && mHead.dueTime <= now;
    }

    //這就是取出消息的方法
    public CallbackRecord extractDueCallbacksLocked(long now) {
        CallbackRecord callbacks = mHead;
        if (callbacks == null || callbacks.dueTime > now) {
            return null;
        }

        CallbackRecord last = callbacks;
        CallbackRecord next = last.next;
        while (next != null) {
            if (next.dueTime > now) {
                last.next = null;
                break;
            }
            last = next;
            next = next.next;
        }
        mHead = next;
        return callbacks;
    }

    //添加消息
    public void addCallbackLocked(long dueTime, Object action, Object token) {...}

    //刪除消息
    public void removeCallbacksLocked(Object action, Object token) {...}
}

跟蹤代碼發(fā)現(xiàn),這個CallbackQueue#extractDueCallbacksLocked()會被Choreographer#doCallbacks()調(diào)用搭综,Choreographer#doCallbacks()又會被Choreographer#doFrame()調(diào)用垢箕,最終我們跟到了FrameDisplayEventReceiver

FrameDisplayEventReceiver

因?yàn)樯厦娴膎ative方法我們沒有跟進(jìn)去分析,擔(dān)心給大家繞暈了兑巾,我們會用一個新的章節(jié)來分析native層做的事情条获,這里先直接給出結(jié)論

nativeScheduleVsync()會向SurfaceFlinger注冊Vsync信號的監(jiān)聽,VSync信號由SurfaceFlinger實(shí)現(xiàn)并定時發(fā)送蒋歌,當(dāng)Vsync信號來的時候就會回調(diào)FrameDisplayEventReceiver#onVsync()帅掘,這個方法給發(fā)送一個帶時間戳Runnable消息,這個Runnable消息的run()實(shí)現(xiàn)就是FrameDisplayEventReceiver# run()堂油, 接著就會執(zhí)行doFrame()

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource);
    }

    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
            scheduleVsync();
            return;
        }
        long now = System.nanoTime();
        if (timestampNanos > now) {
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);
        //設(shè)置異步消息
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

doFrame()會計(jì)算當(dāng)前時間與時間戳的間隔修档,間隔越大表示這一幀處理的時間越久,如果間隔超過一個周期府框,就會去計(jì)算跳過了多少幀萍悴,并打印出一個日志,這個日志我想很多人可能都見過

Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
        + "The application may be doing too much work on its main thread.");

最終doFrame()會從mCallbackQueue 中取出消息并按照時間戳順序調(diào)用mTraversalRunnablerun()函數(shù),mTraversalRunnable就是最初被加入到Choreographer中的Runnable()

//ViewRootImp 
mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

TraversalRunnable

doTraversal()中就會開始我們View的繪制流程癣诱,View的繪制流程不是本文的重點(diǎn)计维,感興趣的可以看最全的View繪制流程(下)— Measure、Layout撕予、Draw

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

ViewRootImp#doTraversal()

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步消息屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

到此為止鲫惶,從觸發(fā)繪制到屏幕真正開始繪制的過程就基本講完了,但是這里還有最后一個細(xì)節(jié)沒有進(jìn)行分析

同步消息屏障

還記不記得前面說有mHandler.getLooper().getQueue().postSyncBarrier()這個方法還沒有進(jìn)行分析实抡,這個方法的作用是什么呢欠母?

void scheduleTraversals() {
    if (!mTraversalScheduled) {         
        mTraversalScheduled = true;
        //☆
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //向Choreographer中發(fā)送消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //...
    }
}

我們知道,Android是基于消息機(jī)制的吆寨,每一個操作都是一個Message赏淌,如果在觸發(fā)繪制的時候,消息隊(duì)列中還有很多消息沒有被執(zhí)行啄清,那是不是意味著要等到消息隊(duì)列中的消息執(zhí)行完成后六水,繪制消息才能被執(zhí)行到,那么依然無法保證Vsync信號和繪制的同步辣卒,所以依然可能出現(xiàn)丟幀的現(xiàn)象

還記不記得我們之前在Choreographer#scheduleFrameLocked()FrameDisplayEventReceiver#onVsync()中提到掷贾,我們會給與Message有關(guān)的繪制請求設(shè)置成異步消息(msg.setAsynchronous(true)),為什么要這么做呢荣茫?這時候MessageQueue#postSyncBarrier()就發(fā)揮它的作用了想帅,簡單來說,它的作用就是一個同步消息屏障啡莉,能夠把我們的異步消息(也就是繪制消息)的優(yōu)先級提到最高

MessageQueue#postSyncBarrier()

主線程的 Looper 會一直循環(huán)調(diào)用 MessageQueuenext() 來取出隊(duì)頭的 Message 執(zhí)行港准,當(dāng) Message 執(zhí)行完后再去取下一個。當(dāng) next() 方法在取 Message 時發(fā)現(xiàn)隊(duì)頭是一個同步屏障的消息時咧欣,就會去遍歷整個隊(duì)列浅缸,只尋找設(shè)置了異步標(biāo)志的消息,如果有找到異步消息该押,那么就取出這個異步消息來執(zhí)行,否則就讓 next() 方法陷入阻塞狀態(tài)阵谚。如果 next() 方法陷入阻塞狀態(tài)蚕礼,那么主線程此時就是處于空閑狀態(tài)的,也就是沒在干任何事梢什。所以奠蹬,如果隊(duì)頭是一個同步屏障的消息的話,那么在它后面的所有同步消息就都被攔截住了嗡午,直到這個同步屏障消息被移除囤躁,否則主線程就一直不會去處理同步屏障后面的同步消息

那這么同步屏障是什么時候被移除的呢?

其實(shí)我們就是在我們上面提到的ViewRootImp#doTraversal()方法中

總結(jié)

本文講了屏幕刷新的基本原理,以及雙緩沖機(jī)制狸演、Choreographer的作用言蛇、同步消息屏障,不同的地方出了問題都可能引起丟幀宵距,所以了解這些細(xì)節(jié)有助于我們更好的排查項(xiàng)目開發(fā)過程中的問題腊尚,最后,來梳理一下屏幕刷新的流程圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載满哪,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者婿斥。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市哨鸭,隨后出現(xiàn)的幾起案子民宿,更是在濱河造成了極大的恐慌,老刑警劉巖像鸡,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件活鹰,死亡現(xiàn)場離奇詭異,居然都是意外死亡坟桅,警方通過查閱死者的電腦和手機(jī)华望,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仅乓,“玉大人赖舟,你說我怎么就攤上這事】溟梗” “怎么了宾抓?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豫喧。 經(jīng)常有香客問我石洗,道長,這世上最難降的妖魔是什么紧显? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任讲衫,我火速辦了婚禮,結(jié)果婚禮上孵班,老公的妹妹穿的比我還像新娘涉兽。我一直安慰自己,他們只是感情好篙程,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布枷畏。 她就那樣靜靜地躺著,像睡著了一般虱饿。 火紅的嫁衣襯著肌膚如雪拥诡。 梳的紋絲不亂的頭發(fā)上触趴,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音渴肉,去河邊找鬼冗懦。 笑死,一個胖子當(dāng)著我的面吹牛宾娜,可吹牛的內(nèi)容都是我干的批狐。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼前塔,長吁一口氣:“原來是場噩夢啊……” “哼嚣艇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起华弓,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤食零,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后寂屏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贰谣,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年迁霎,在試婚紗的時候發(fā)現(xiàn)自己被綠了吱抚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡考廉,死狀恐怖秘豹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昌粤,我是刑警寧澤既绕,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站涮坐,受9級特大地震影響凄贩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜袱讹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一疲扎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捷雕,春花似錦椒丧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盹廷。三九已至征绸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背管怠。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工淆衷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人渤弛。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓祝拯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親她肯。 傳聞我的和親對象是個殘疾皇子佳头,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 更新:最近發(fā)現(xiàn)該篇文章閱讀的人數(shù)挺多,并且也發(fā)現(xiàn)了更好的關(guān)于Android顯示系統(tǒng)的文章晴氨,所以將關(guān)于Android...
    htkeepmoving閱讀 9,652評論 0 10
  • 1康嘉、概述 不論電腦,電視籽前,手機(jī)亭珍,我們看到的畫面都是由一幀幀的畫面組成的。FPS是圖像領(lǐng)域中的定義枝哄,是指畫面每秒傳輸...
    高丕基閱讀 12,019評論 6 34
  • UI 優(yōu)化系列專題肄梨,來聊一聊 Android 渲染相關(guān)知識,主要涉及 UI 渲染背景知識挠锥、如何優(yōu)化 UI 渲染兩部...
    godliness閱讀 3,114評論 0 6
  • 轉(zhuǎn)載請注明出處:http://blog.csdn.net/a740169405/article/details/7...
    良秋閱讀 14,059評論 8 81
  • 常用mysql相關(guān)命令 連接數(shù)據(jù)庫 查看版本众羡,時間 查看有哪些數(shù)據(jù)庫 查看現(xiàn)在使用的數(shù)據(jù)庫和登錄用戶: 使用zab...
    AK蝸牛閱讀 652評論 0 0