Android之卡頓監(jiān)聽

一、背景

在用戶使用APP過程中,為保證應(yīng)用的平滑性,每一幀渲染時間不能超過16.7ms,達(dá)到60幀每秒;如果UI渲染慢的話,就會發(fā)生丟幀,這樣用戶就會感覺到不連貫性、卡頓現(xiàn)象??D容易被用戶直觀感受到,且造成卡頓的原因錯綜復(fù)雜,定位困難,很多平臺都要對用戶卡頓信息收集上報,用于卡頓問題的故障排查,指導(dǎo)APP流暢度優(yōu)化

二、原理


CPU部分: 邏輯的計算,例如:計算布局,解碼圖片,創(chuàng)建視圖,繪制文本,計算好將要顯示的內(nèi)容轉(zhuǎn)交給GPU;

GPU部分: GPU開始變換,合成,渲染后將結(jié)果換到幀緩沖區(qū),隨后視頻控制器從幀緩沖區(qū)中讀取數(shù)據(jù),經(jīng)過一系列的轉(zhuǎn)換后交給顯示器進(jìn)行顯示;

幀率FPS: Frames Per Second 的簡稱縮寫,每秒傳輸幀數(shù),F(xiàn)PS值越低越卡頓,所以這個值在一定程度上可以衡量應(yīng)用在圖像繪制渲染處理時的性能。60fps 最佳,一般我們的APP的FPS 只要保持在 50-60之間,用戶體驗都是比較流暢的。FPS=60時,1000/60≈16.7,大概16ms中,進(jìn)行一次屏幕的刷新繪制;

VSync: Vertical Synchronization的簡稱縮寫,可以簡單的理解成一個時間中斷。例如,每16ms會有一個Vsync信號,那么系統(tǒng)在每次拿到Vsync信號時刷新屏幕。 從上圖中看出,每兩個VSync信號之間有時間間隔(16.7ms),在這個時間內(nèi),若CPU跟GPU進(jìn)行界面渲染,計算跟繪制,讓界面的幀率在1秒內(nèi)達(dá)到60fps,則視覺上APP流暢度好,若在16ms內(nèi)不能完成界面的渲染,計算跟繪制,就會產(chǎn)生丟幀的現(xiàn)象,丟幀就會造成應(yīng)用卡頓現(xiàn)象。

三、方案

通過原理我們可以很清楚的知道,如果能夠統(tǒng)計系統(tǒng)繪制每一幀的起始時間,自然就能統(tǒng)計到目標(biāo)視圖渲染的耗時。如果制定相應(yīng)的規(guī)則,便可以捕捉到整個應(yīng)用的丟幀狀況,進(jìn)而做出改善。
Android系統(tǒng)就提供了這樣的類,用于監(jiān)聽每一幀的繪制信息,這就是Choreographer。接下來從源碼進(jìn)度分析監(jiān)聽過程。

四、源碼分析

Choreographer提供了一個叫FrameCallback的回調(diào)接口,從官方定義可以知道,每渲染新的一幀,實現(xiàn)這個接口的類都會接收到回調(diào)?;卣{(diào)是在持有Choreographer的線程的Looper循環(huán)里完成的,調(diào)用的具體方法就是doFrame

    /**
     * Implement this interface to receive a callback when a new display frame is
     * being rendered.  The callback is invoked on the {@link Looper} thread to
     * which the {@link Choreographer} is attached.
     */
    public interface FrameCallback {
        /**
         * Called when a new display frame is being rendered.
         * <p>
         * This method provides the time in nanoseconds when the frame started being rendered.
         * The frame time provides a stable time base for synchronizing animations
         * and drawing.  It should be used instead of {@link SystemClock#uptimeMillis()}
         * or {@link System#nanoTime()} for animations and drawing in the UI.  Using the frame
         * time helps to reduce inter-frame jitter because the frame time is fixed at the time
         * the frame was scheduled to start, regardless of when the animations or drawing
         * callback actually runs.  All callbacks that run as part of rendering a frame will
         * observe the same frame time so using the frame time also helps to synchronize effects
         * that are performed by different callbacks.
         * </p><p>
         * Please note that the framework already takes care to process animations and
         * drawing using the frame time as a stable time base.  Most applications should
         * not need to use the frame time information directly.
         * </p>
         *
         * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
         * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
         * to convert it to the {@link SystemClock#uptimeMillis()} time base.
         */
        public void doFrame(long frameTimeNanos);
    }

有了每一幀渲染時的回調(diào)接口及繪制起始時間,系統(tǒng)只需要在相鄰兩幀之間發(fā)消息,就可以統(tǒng)計出渲染時長,通過跟標(biāo)準(zhǔn)幀率(16.7ms/幀)對比,卡頓情況就一目了然了。
為了方便用戶調(diào)用,Choreographer類提供了postFrameCallback和postFrameCallbackDelayed兩個方法用來將當(dāng)前頁面的繪制時刻回傳到下一幀。

    /**
     * Posts a frame callback to run on the next frame.
     * <p>
     * The callback runs once then is automatically removed.
     * </p>
     *
     * @param callback The frame callback to run during the next frame.
     *
     * @see #postFrameCallbackDelayed
     * @see #removeFrameCallback
     */
    public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }

    /**
     * Posts a frame callback to run on the next frame after the specified delay.
     * <p>
     * The callback runs once then is automatically removed.
     * </p>
     *
     * @param callback The frame callback to run during the next frame.
     * @param delayMillis The delay time in milliseconds.
     *
     * @see #postFrameCallback
     * @see #removeFrameCallback
     */
    public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }

這倆方法是兄弟倆,都是把FrameCallback拋到下一幀,區(qū)別在于后者有延時。無論如何最終都會走postFrameCallbackDelayed方法,該方法內(nèi)部調(diào)用了postCallbackDelayedInternal方法,邏輯如下:

 private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
     

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);  // 代碼1

            if (dueTime <= now) {  // 代碼2
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

action就是上面?zhèn)魅氲腸allback,代碼1處對回調(diào)做了存儲,追蹤代碼會發(fā)現(xiàn)其實是存在了一個叫CallbackRecord的類中,然后CallbackRecord作為鏈表的一個節(jié)點存儲。

        public void addCallbackLocked(long dueTime, Object action, Object token) {
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            CallbackRecord entry = mHead;
            if (entry == null) {
                mHead = callback;
                return;
            }
            if (dueTime < entry.dueTime) {
                callback.next = entry;
                mHead = callback;
                return;
            }
            while (entry.next != null) {
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break;
                }
                entry = entry.next;
            }
            entry.next = callback;
        }

回歸到postCallbackDelayedInternal方法的代碼2,由于我們以非延時執(zhí)行邏輯為主線,系統(tǒng)會執(zhí)行scheduleFrameLocked方法:

private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame on vsync.");
                }

                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    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);
            }
        }
    }

系統(tǒng)根據(jù)是否使用VSYNC垂直同步信號,判斷是否借助VSYNC信號進(jìn)行渲染。這里分析VSYNC信號的情況,系統(tǒng)最終都會調(diào)用scheduleVsyncLocked方法,并將DisplayEventReceiver的引用傳遞給native層。

    /**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    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);
        }
    }

注意該方法的定義,計劃在下一個顯示幀開始時傳遞單個垂直同步脈沖。也就是說,下一幀開始時,才會給當(dāng)前幀發(fā)送一個垂直同步脈沖信號VSYNC,相鄰兩幀的時間差值就是繪制時間。那么系統(tǒng)是如何把VSYNC回傳的呢?
其實FrameDisplayEventReceiver是DisplayEventReceiver派生類,而且它實現(xiàn)了runnable接口,在構(gòu)建DisplayEventReceiver實例時,就已經(jīng)消息隊列傳遞給native層。

   /**
     * Creates a display event receiver.
     *
     * @param looper The looper to use when invoking callbacks.
     * @param vsyncSource The source of the vsync tick. Must be on of the VSYNC_SOURCE_* values.
     */
    public DisplayEventReceiver(Looper looper, int vsyncSource) {
        if (looper == null) {
            throw new IllegalArgumentException("looper must not be null");
        }

        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
                vsyncSource);

        mCloseGuard.open("dispose");
    }

底層在開始第二幀時將VSYNC信號放到消息隊列中,并通過dispatchVsync完成分發(fā)任務(wù)。

    // Called from native code.
    @SuppressWarnings("unused")
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }

這里完成了一次承接,最終由FrameDisplayEventReceiver的onVsync方法完成消息處理。

 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) {
            // Ignore vsync from secondary display.
            // This can be problematic because the call to scheduleVsync() is a one-shot.
            // We need to ensure that we will still receive the vsync from the primary
            // display which is the one we really care about.  Ideally we should schedule
            // vsync for a particular display.
            // At this time Surface Flinger won't send us vsyncs for secondary displays
            // but that could change in the future so let's log a message to help us remember
            // that we need to fix this.
            if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                Log.d(TAG, "Received vsync from secondary display, but we don't support "
                        + "this case yet.  Choreographer needs a way to explicitly request "
                        + "vsync for a specific display to ensure it doesn't lose track "
                        + "of its scheduled vsync.");
                scheduleVsync();
                return;
            }

            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) {
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }

            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

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

onVsync會過濾掉一次無效信號,然后通過FrameHandler將消息發(fā)送到自己的消息隊列,并調(diào)用自己的run方法。詳細(xì)請參考《Android面試中的handler》
在run中執(zhí)行了 doFrame(mTimestampNanos, mFrame)邏輯,將最新的時間戳傳回。

    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

                               ......

            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {  // 代碼1
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                            + "which is more than the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Skipping " + skippedFrames + " frames and setting frame "
                            + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
                }
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {  // 代碼2
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                scheduleVsyncLocked();
                return;
            }

            if (mFPSDivisor > 1) {  
                long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
                if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                    scheduleVsyncLocked();
                    return;
                }
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos; // 代碼3
        }

        try { // 代碼4
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }

代碼1處,通過與標(biāo)準(zhǔn)單幀耗時進(jìn)行比較,如果信號發(fā)出到執(zhí)行至代碼1的時間耗時超過標(biāo)準(zhǔn)幀間距,說明主線程任務(wù)量大,需要舍棄(long)(jitterNanos / mFrameIntervalNanos)幀,并且取余獲得補(bǔ)償系數(shù),修正frameTimeNanos這個起始時間。這就是所謂的“丟幀”。
代碼2處,如果修正后的起始時間frameTimeNanos小于上一幀的起始時間mLastFrameTimeNanos,說明丟幀造成時間戳后移,只能等待下一次VSYNC信號。同時,調(diào)用scheduleVsyncLocked觸發(fā)二次信號,return結(jié)束本次同步。
代碼3處,對當(dāng)前幀的起始時間做了保存,根據(jù)代碼2處的返回終止可以知道,相鄰兩次frameTimeNanos和mLastFrameTimeNanos的差值可以很大,因為存在不保存frameTimeNanos的情況。
經(jīng)過以上分析,我們可以明白,只要記錄兩次時間取差值,就可以監(jiān)聽頁面卡頓了。那么如何實現(xiàn)自定義規(guī)則呢?
代碼4的try方法連續(xù)調(diào)用了4次doCallbacks方法,分別傳入不同的回調(diào)類型Input callback、Animation callback、Traversal callback、 Commit callback

    /**
     * Callback type: Input callback.  Runs first.
     * @hide
     */
    public static final int CALLBACK_INPUT = 0;

    /**
     * Callback type: Animation callback.  Runs before traversals.
     * @hide
     */
    @TestApi
    public static final int CALLBACK_ANIMATION = 1;

    /**
     * Callback type: Traversal callback.  Handles layout and draw.  Runs
     * after all other asynchronous messages have been handled.
     * @hide
     */
    public static final int CALLBACK_TRAVERSAL = 2;

    /**
     * Callback type: Commit callback.  Handles post-draw operations for the frame.
     * Runs after traversal completes.  The {@link #getFrameTime() frame time} reported
     * during this callback may be updated to reflect delays that occurred while
     * traversals were in progress in case heavy layout operations caused some frames
     * to be skipped.  The frame time reported during this callback provides a better
     * estimate of the start time of the frame in which animations (and other updates
     * to the view hierarchy state) actually took effect.
     * @hide
     */
    public static final int CALLBACK_COMMIT = 3;

doCallback方法分別對這四種類型處理,無論哪種類型最終都會調(diào)用CallbackRecord的run方法。代碼如下:

   /**
     * @param callbackType: Input callback、Animation callback、Traversal callback、 Commit callback
     */
    void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            mCallbacksRunning = true;
        
                        ......

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                c.run(frameTimeNanos);  // 代碼1
            }
        } finally {
            ,,,,,,
    }

代碼1處,調(diào)用了CallbackRecord的run方法,而CallbackRecord#run的具體實現(xiàn)如下:

    private static final class CallbackRecord {
        public CallbackRecord next;
        public long dueTime;
        public Object action; // Runnable or FrameCallback
        public Object token;

        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
    }

其實是執(zhí)行了FrameCallback#doFrame方法,終于回到了開頭。我們只需要實現(xiàn)FrameCallback接口,并在合適的位置通過Choreographer.getInstance().postFrameCallback(this.mFrameCallback);將其拋出,記錄兩次frameTimeNanos的差值,即可完成時間戳的統(tǒng)計,實現(xiàn)監(jiān)聽當(dāng)前線程卡頓的目的。

五、卡頓場景

1. 死鎖

在多線程場景下,Thread1和Thread2相互依賴,造成死鎖堵塞整個線程的執(zhí)行。以CountdownLatch為例,如下demo:

    private void CountdownLatchDemo() {
        CountDownLatch mCountDownLatch = new CountDownLatch(3);  // 代碼1
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                    mCountDownLatch.countDown();
                } catch (InterruptedException e) {
                    mCountDownLatch.countDown();
                }
            }
        });
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                    mCountDownLatch.countDown();
                } catch (InterruptedException e) {
                    mCountDownLatch.countDown();
                }
            }
        });

        try {
            mCountDownLatch.await(); // 代碼2
        } catch (InterruptedException e) {
            Log.d(TAG,"countDownLatchException = " + e.getMessage());
        }
    }

由于代碼1處初始化CountdownLatch是3,實際執(zhí)行過程中只開了兩個線程,造成代碼2處一直處于等待狀態(tài),由于Activity阻塞主線程,5s之后便報了anr崩潰。log如下:


主線程阻塞造成ANR

2. 布局過深

布局層級過深,重復(fù)繪制

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <!-- 嵌套視圖組件,可以有很多 -->
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Hello World!" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Hello World!" />

            <!-- 更多嵌套視圖組件... -->

        </LinearLayout>
    </ScrollView>
</RelativeLayout>

3. 頻繁創(chuàng)建局部變量

onDraw等頻繁調(diào)用的方法中什么局部變量,造成頻繁gc回收,引發(fā)內(nèi)存抖動

public class CustomView extends View {
    private Paint paint = new Paint();

    public CustomView(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        for (int i = 0; i < 1000; i++) {
            // 在循環(huán)中頻繁創(chuàng)建臨時的Rect對象
            Rect rect = new Rect(i, i, getWidth() - i, getHeight() - i);
            canvas.drawRect(rect, paint);
        }
    }
}

4. 視頻過大

在線播放視頻,視頻源過大,造成卡頓【可以通過下載視頻到本地解決】

六、卡頓監(jiān)控方案

  1. Choreographer原理及應(yīng)用:https://zhuanlan.zhihu.com/p/362334212
  2. 卡頓監(jiān)測 · 方案篇 · Android卡頓監(jiān)測指導(dǎo)原則:https://juejin.cn/post/7214635327407308859
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容