天天看點

Adnroid 卡頓分析與布局優化

1 卡頓分析

1 Systrace

Systrace是Android平台提供的一款工具,用于記錄短期内的裝置活動,其中彙總了Android核心中的資料,例如CPU排程程式,磁盤活動和應用程式,Systrace主要用來分析繪制性能方面的問題,在發生卡頓時,通過這份報告,可以知道目前整個系統所處的狀态,進而幫助開發者更直覺的分析系統瓶頸,改進系統性能`

2 android profile 中的cpu監測

**

App層面監測卡頓

1 利用UI線程的Looper列印日志比對

2 使用Choreographer.FrameCallback

Looper日志監測卡頓**

Android 主線程更新UI,如果界面1室内重新整理少于60次,即FPS小于60,使用者就會産生卡頓的感覺,簡單來說Android使用消息機制進行UI更新,UI線程有個Looper,在其loop方法中會不斷去除message,調用其他綁定的UI線程執行,如果在handler的dispatchMessage方法裡面有耗時操作,就會發生卡頓,

隻要監測msg.target.dispatchmessage的執行時間,就能檢車就能檢測到部分UI線程是否有耗時的操作。注意到這行

執行代碼的前後,有兩個logging.println函數,如果設定了logging,會分别列印出>>>>> Dispatching to和

<<<<< Finished to 這樣的日志,這樣我們就可以通過兩次log的時間內插補點,來計算dispatchMessage的執行時

間,進而設定門檻值判斷是否發生了卡頓。

Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,是以我們可以自己實作一個Printer,在

通過setMessageLogging()方法傳入即可:

package com.dy.safetyinspectionforengineer.block;

import android.os.Looper;
public class BlockCanary {
    public static void install() {
        LogMonitor logMonitor = new LogMonitor();
        Looper.getMainLooper().setMessageLogging(logMonitor);
    }
}
           
package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Printer;
import java.util.List;

public class LogMonitor implements Printer {

    private StackSampler mStackSampler;
    private boolean mPrintingStarted = false;
    private long mStartTimestamp;
    // 卡頓門檻值
    private long mBlockThresholdMillis = 3000;
    //采樣頻率
    private long mSampleInterval = 1000;

    private Handler mLogHandler;

    public LogMonitor() {
        mStackSampler = new StackSampler(mSampleInterval);
        HandlerThread handlerThread = new HandlerThread("block-canary-io");
        handlerThread.start();
        mLogHandler = new Handler(handlerThread.getLooper());
    }
    @Override
    public void println(String x) {
        //從if到else會執行 dispatchMessage,如果執行耗時超過門檻值,輸出卡頓資訊
        if (!mPrintingStarted) {
            //記錄開始時間
            mStartTimestamp = System.currentTimeMillis();
            mPrintingStarted = true;
            mStackSampler.startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //出現卡頓
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            mStackSampler.stopDump();
        }
    }
    private void notifyBlockEvent(final long endTime) {
        mLogHandler.post(new Runnable() {
            @Override
            public void run() {
                //獲得卡頓時主線程堆棧
                List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
                for (String stack : stacks) {
                    Log.e("block-canary", stack);
                }
            }
        });
    }
    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
}
           
package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class StackSampler {
    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
            new SimpleDateFormat("MM-dd HH:mm:ss.SSS");

    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>();
    private int mMaxCount = 100;
    private long mSampleInterval;
    //是否需要采樣
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
        mSampleInterval = sampleInterval;
        HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
    }
    /**
     * 開始采樣 執行堆棧
     */
    public void startDump() {
        //避免重複開始
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);
        mHandler.removeCallbacks(mRunnable);
        mHandler.postDelayed(mRunnable, mSampleInterval);
    }
    public void stopDump() {
        if (!mShouldSample.get()) {
            return;
        }
        mShouldSample.set(false);
        mHandler.removeCallbacks(mRunnable);
    }
    public List<String> getStacks(long startTime, long endTime) {
        ArrayList<String> result = new ArrayList<>();
        synchronized (mStackMap) {
            for (Long entryTime : mStackMap.keySet()) {
                if (startTime < entryTime && entryTime < endTime) {
                    result.add(TIME_FORMATTER.format(entryTime)
                            + SEPARATOR
                            + SEPARATOR
                            + mStackMap.get(entryTime));
                }
            }
        }
        return result;
    }
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString()).append("\n");
            }
            synchronized (mStackMap) {
                //最多儲存100條堆棧資訊
                if (mStackMap.size() == mMaxCount) {
                    mStackMap.remove(mStackMap.keySet().iterator().next());
                }
                mStackMap.put(System.currentTimeMillis(), sb.toString());
            }
            if (mShouldSample.get()) {
                mHandler.postDelayed(mRunnable, mSampleInterval);
            }
        }
    };
}
           
public class MyApplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        BlockCanary.install();
    }
}
           

Choreographer.FrameCallback

Android系統每隔16ms發出VSYNC信号,來通知界面進行重繪、渲染,每一次同步的周期約為16.6ms,代表一幀

的重新整理頻率。通過Choreographer類設定它的FrameCallback函數,當每一幀被渲染時會觸發回調

FrameCallback.doFrame (long frameTimeNanos) 函數。frameTimeNanos是底層VSYNC信号到達的時間戳 。

import android.os.Build;
import android.view.Choreographer;

import java.util.concurrent.TimeUnit;

public class ChoreographerHelper {

    static long lastFrameTimeNanos = 0;

    public static void start() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {

                @Override
                public void doFrame(long frameTimeNanos) {
                    //上次回調時間
                    if (lastFrameTimeNanos == 0) {
                        lastFrameTimeNanos = frameTimeNanos;
                        Choreographer.getInstance().postFrameCallback(this);
                        return;
                    }
                    long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
                    if (diff > 16.6f) {
                        //掉幀數
                        int droppedCount = (int) (diff / 16.6);
                    }
                    lastFrameTimeNanos = frameTimeNanos;
                    Choreographer.getInstance().postFrameCallback(this);
                }
            });
        }
    }
}

           

通過 ChoreographerHelper 可以實時計算幀率和掉幀數,實時監測App頁面的幀率資料,發現幀率過低,還可以自

動儲存現場堆棧資訊。

Looper比較适合在釋出前進行測試或者小範圍灰階測試然後定位問題,ChoreographerHelper适合監控線上環境

的 app 的掉幀情況來計算 app 在某些場景的流暢度然後有針對性的做性能優化。

布局優化

1 層級優化

可以使用工具layoutinspector 檢視層級,或這看源碼檢視層級

Tools - layoutINspector

2 使用merge标簽

當我們有一些布局元素需要被多處使用時,我們可以将其抽取成一個單獨的布局檔案,在需要的地方include加載,這是就可以使用merge标簽,吧這些抽離的标簽進行包裹

3 使用viewstub标簽

在不顯示及不可見的情況下 用viewstub來包裹,被包裹後,如果visible=gone 則該view不會立即加載,等到需要顯示的時候,設定viewstub為visible 或調用其inflater()方法,該view才會初始化

過度渲染

1進入開發則選項

2調用調試GPU過度繪制

3 選擇顯示過度繪制區域

3.1 藍色 為一次繪制 綠色為兩次繪制 粉色為3次繪制,紅色為4次或更多次繪制

解決過度繪制問題

1 移除不需要的背景

2 使視圖層次結構扁平化

3 降低透明度

布局加載優化

1 異步加載

setContentView 時 可以異步加載

implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" 

 new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, 
                new  AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int resid, 
                                          @Nullable ViewGroup parent) {
                setContentView(view);
            }
        });
           

2 掌閱X2C思路

通過注解,把XM代碼編輯成java代碼

https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md