首頁 > 軟體

Android 執行緒優化知識點學習

2022-08-11 18:01:32

前言

在實際專案開發中會頻繁的用到執行緒,執行緒使用起來是很簡單,但是濫用執行緒會帶來效能問題, 比如啟動一個執行緒至少 佔用16kb的記憶體、執行緒過多會導致cpu的頻繁切換而cpu切換成本是很高的、消耗大量使用者電量等問題, 所以應該讓app的執行緒數保持在合理水平,這是效能優化中很重要的一部分。本文對執行緒優化方面的知識做了一個全面總結,主要內容如下:

一、執行緒排程原理解析

執行緒排程的原理

在任意時刻,CPU 只能執行一條機器指令,每個執行緒只有獲得了 CPU 的使用權之後才能執行指令,也就是說 在任意時刻,只有一個執行緒佔用 CPU,處於執行狀態。而我們平常所說的 多執行緒並行執行,實際上說的是多個執行緒輪流獲取 CPU 的使用權,然後分別執行各自的任務。其實在可執行池當中有多個處於就緒狀態的執行緒在等待 CPU,而 JVM 負責執行緒排程,按照特定機制為多個執行緒分配 CPU 使用權。

上面的描述提到了三個主要資訊:

  • 在任意時刻,只有一個執行緒佔用 CPU,處於執行狀態
  • 多執行緒並行執行,實際上說的是多個執行緒輪流獲取 CPU 的使用權
  • JVM 負責執行緒排程,按照特定機制為多個執行緒分配 CPU 使用權

執行緒排程模型

執行緒排程模型可以分為兩類,分別是 分時排程模型 和 搶佔式排程模型。

  • 分時排程模型:讓所有執行緒輪流獲取 CPU 的使用權,而且均分每個執行緒佔用 CPU 的時間片,這種方式非常公平
  • 搶佔式排程模型:JVM 使用的是搶佔式排程模型,讓優先順序高的執行緒優先獲取到 CPU 的使用權,如果在可執行池當中的執行緒優先順序都一樣,那就隨機選取一個

Android 的執行緒排程

Android 的執行緒排程從兩個因素決定,一個是 nice 值(即執行緒優先順序),一個是 cgroup(即執行緒排程策略)。

對於 nice 值來說,它首先是在 Process 中定義的,值越小,程序優先順序越高,預設值是 THREAD_PRIORITY_DEFAULT = 0,主執行緒的優先順序也是這個值。修改 nice 值只需要在對應的執行緒下設定即可:

public class MyRunnable implements Runnable {<!-- -->
	@Override
	public void run() {<!-- -->
		Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT)
	}
}
// 附上 setThreadPriority() 檔案說明
/**
 * Set the priority of the calling thread, based on Linux priorities.  See
 * {@link #setThreadPriority(int, int)} for more information.
 * 
 * @param priority A Linux priority level, from -20 for highest scheduling
 * priority to 19 for lowest scheduling priority.
 * 
 * @throws IllegalArgumentException Throws IllegalArgumentException if
 * &lt;var&gt;tid&lt;/var&gt; does not exist.
 * @throws SecurityException Throws SecurityException if your process does
 * not have permission to modify the given thread, or to use the given
 * priority.
 * 
 * @see #setThreadPriority(int, int)
 */
public static final native void setThreadPriority(int priority)
        throws IllegalArgumentException, SecurityException;

nice 值它還有其他的優先順序可選:

public class Process {
    /**
     * Standard priority of application threads.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
	public static final int THREAD_PRIORITY_DEFAULT = 0;
    /**
     * Lowest available thread priority.  Only for those who really, really
     * don't want to run if anything else is happening.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_LOWEST = 19;	
    /**
     * Standard priority background threads.  This gives your thread a slightly
     * lower than normal priority, so that it will have less chance of impacting
     * the responsiveness of the user interface.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_BACKGROUND = 10;    
    /**
     * Standard priority of threads that are currently running a user interface
     * that the user is interacting with.  Applications can not normally
     * change to this priority; the system will automatically adjust your
     * application threads as the user moves through the UI.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_FOREGROUND = -2;
    /**
     * Standard priority of system display threads, involved in updating
     * the user interface.  Applications can not
     * normally change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_DISPLAY = -4;    
    /**
     * Standard priority of the most important display threads, for compositing
     * the screen and retrieving input events.  Applications can not normally
     * change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;
    /**
     * Standard priority of video threads.  Applications can not normally
     * change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_VIDEO = -10;
    /**
     * Standard priority of audio threads.  Applications can not normally
     * change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_AUDIO = -16;
    /**
     * Standard priority of the most important audio threads.
     * Applications can not normally change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_URGENT_AUDIO = -19;
    /**
     * Minimum increment to make a priority more favorable.
     */
    public static final int THREAD_PRIORITY_MORE_FAVORABLE = -1;
    /**
     * Minimum increment to make a priority less favorable.
     */
    public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1;    
}

在實踐過程當中,如果只有 nice 值是不足夠的。比如有一個 app 它有1個前臺執行緒,而且它還有10個後臺執行緒,雖然後臺執行緒的優先順序比較低,但是數量比較多,這10個後臺執行緒對 CPU 的消耗量是可以影響到前臺執行緒的效能的。所以 Android 需要一種機制來處理這種情況,也就是 cgroup。

Android 借鑑了 Linux 的 cgroup 來執行 更嚴格的前臺和後臺排程策略,後臺優先順序的執行緒會被隱式的移動到後臺 group,而其他 group 的執行緒如果處於工作狀態,那麼後臺這些執行緒它們將會被限制,只有很小的機率能夠利用 CPU。

這種分離的排程策略既允許了後臺執行緒來執行一些任務,同時又不會對使用者可見的前臺執行緒造成很大的影響,讓前臺執行緒有更多的 CPU。

或許你會有疑問:哪些執行緒會被移到後臺 group?

  • 第一種就是那些 手動設定了優先順序比較低的執行緒
  • 第二種就是 不在前臺執行的那些應用程式的執行緒

執行緒排程小結

  • 執行緒過多會導致 CPU 頻繁切換,降低執行緒執行效率。 在前面講解啟動優化的時候有強調要充足的利用執行緒比如非同步啟動任務,但是執行緒也不能無限制的使用
  • 正確認識任務重要性決定哪種優先順序。 一般情況下執行緒工作量和優先順序是成反比,比如執行緒的工作量越大,所做的工作沒那麼重要,那這個執行緒的優先順序應該越低
  • 執行緒的優先順序具有繼承性。 比如在 A 執行緒建立了 B 執行緒,在我們沒有指定執行緒優先順序的情況下,B 執行緒的優先順序是和 A 一樣的。所以我們在 UI 執行緒中建立執行緒,執行緒的優先順序是和 UI 執行緒一樣的,這就會導致 UI 執行緒搶佔 CPU 時間片的概率會變少

二、Android 非同步方式彙總

Thread

使用 Thread 建立執行緒是最簡單、常見的非同步方式,但在實際專案中,它也就只有這個優點了,並不推薦直接使用 Thread 建立執行緒,主要有以下幾點原因:

  • 不易複用,頻繁建立及銷燬開銷大
  • 複雜場景不易使用

HandlerThread

是 Android 提供的一個自帶訊息迴圈的執行緒,它內部使用 序列的方式執行任務,比較 適合長時間執行,不斷從佇列中獲取任務的場景。

IntentService

繼承了 Android Service 元件,內部建立了 HandlerThread,相比 Service 是在主執行緒執行,IntentService 是 在子執行緒非同步執行不佔用主執行緒,而且 優先順序比較高,不易被系統 kill。

AsyncTask

AsyncTask 是 Android 提供的工具類,內部的實現是使用了執行緒池,它比較大的好處是無需自己處理執行緒切換,但需要注意 AsyncTask 不同版本執行方式不一致的問題。

執行緒池

java 提供了執行緒池,在實際專案中比較推薦使用執行緒池的方式實現非同步任務,它主要有以下優點:

  • 易複用,減少執行緒頻繁建立、銷燬的時間
  • 功能強大:定時、任務佇列、並行數控制等,java 提供了 Executors 工具類可以很方便的建立一個執行緒池,也可以自己客製化執行緒池

RxJava

RxJava 由強大的 Scheduler 集合提供,內部實際也是使用的執行緒池,它封裝的非常完善,可以根據任務型別的不同指定使用不同的執行緒池,比如 IO 密集型的任務可以指定 Schedulers.IO,CPU 密集型任務可以指定 Schedulers.Computation

Single.just(xxx)
	.subscribeOn(Schedulers.IO) // 指定工作執行緒型別為 IO 密集型
	.observeOn(AndroidSchedulers.mainThread()) // 指定下游接收所線上程
	.subscribe();

三、Android執行緒優化實戰

執行緒使用準則

  • 嚴禁使用直接new Thread()的方式
  • 提供基礎執行緒池供各個業務線使用: 避免各個業務線各自維護一套執行緒池,導致執行緒數過多
  • 根據任務型別選擇合適的非同步方式: 比如優先順序低且長時間執行可以使用Handler Thread,再比如:有一個任務需要定時執行,使用執行緒池更適合
  • 建立執行緒必須命名: 方便定位執行緒歸屬於哪一個業務方,線上程執行期可以使用Thread.currentThread().setName修改名字
  • 關鍵非同步任務監控: 非同步不等於不耗時,如果一個任務在主執行緒需要耗費500ms,那麼它在非同步任務中至少需要500ms,因為非同步任務中優先順序較低,耗費時間很可能會高於500ms,所以這裡可以使用AOP的方式來做監控,並且結合所在的業務場景,根據監控結果來適時的做一些相對應的調整
  • 重視優先順序設定: 使用Process.setThreadPriority();設定,並且可以設定多次

執行緒池優化實戰

接下來針對執行緒池的使用來做一個簡單的實踐,還是開啟我們之前的專案,這裡說一下每次實踐的程式碼都是基於第一篇啟動優化的那個案例上寫的。

首先新建一個包async,然後在包中建立一個類ThreadPoolUtils,這裡我們建立可重用且固定執行緒數的執行緒池,核心數為5,並且對外暴露一個get方法,然後我們可以在任何地方都能獲取到這個全域性的執行緒池:

public class ThreadPoolUtils {
    //建立定長執行緒池,核心數為5
    private static ExecutorService mService = Executors.newFixedThreadPool(5, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(runnable,"ThreadPoolUtils");//設定執行緒名
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //設定執行緒優先順序
            return thread;
        }
    });
    //獲取全域性的執行緒池
    public static ExecutorService getService(){
        return mService;
    }
}

然後使用的時候就可以在你需要的地方直接呼叫了,並且你在使用的時候還可以修改執行緒的優先順序以及執行緒名稱:

        //使用全域性統一的執行緒池
        ThreadPoolUtils.getService().execute(new Runnable() {
            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); //修改執行緒優先順序
                String oldName = Thread.currentThread().getName();
                Thread.currentThread().setName("Jarchie"); //修改執行緒名稱
                Log.i("MainActivity","");
                Thread.currentThread().setName(oldName); //將原有名稱改回去
            }
        });

四、定位執行緒建立者

如何確定執行緒建立者

當你的專案做的越來越大的時候一般情況下執行緒都會變的非常多,最好是能夠對整體的執行緒數進行收斂,那麼問題來了,如何知道某個執行緒是在哪裡建立的呢?不僅僅是你自己的專案原始碼,你依賴的第三方庫、aar中都有執行緒的建立,如果單靠人眼review程式碼的方式,工作量很大而且你還不一定能找的全。

並且你這次優化完了執行緒數,你還要考慮其他人新加的執行緒是否合理,所以就需要能夠建立一套很好的監控預防手段。然後針對這些情況來做一個解決方案的總結分析,主要思想就是以下兩點:

  • 建立執行緒的位置獲取堆疊
  • 所有的非同步方式,都會走到new Thread

解決方案:

  • 特別適合Hook手段
  • 找Hook點:建構函式或者特定方法
  • Thread的建構函式

可以在建構函式中加上自己的邏輯,獲取當前的呼叫棧資訊,拿到呼叫棧資訊之後,就可以分析看出某個執行緒是否使用的是統一的執行緒池,也可以知道某個執行緒具體屬於哪個業務方。

Epic實戰

Epic簡介

  • Epic是一個虛擬機器器層面、以Java Method為粒度的執行時Hook框架
  • 支援Android4.0-10.0(我的手機上程式出現了閃退,後來查詢原因發現這個庫開源版本一些高版本手機好像不支援)

Epic使用

  • implementation 'me.weishu:epic:0.6.0'
  • 繼承XC_MethodHook,實現相應邏輯
  • 注入Hook:DexposedBridge.findAndHookMethod

程式碼中使用

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //Hook Thread類別建構函式,兩個引數:需要Hook的類,MethodHook的回撥
        DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
            //afterHookedMethod是Hook此方法之後給我們的回撥
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param); //Hook完成之後會回撥到這裡
                //實現自己的邏輯,param.thisObject可以拿到執行緒物件
                Thread thread = (Thread) param.thisObject;
                //Log.getStackTraceString列印當前的呼叫棧資訊
                Log.i(thread.getName() + "stack", Log.getStackTraceString(new Throwable()));
            }
        });
    }

如果你的手機支援的話,這個時候執行程式應該就可以看到執行緒列印出來的堆疊資訊了

五、優雅實現執行緒收斂

執行緒收斂常規方案

  • 根據執行緒建立堆疊考量合理性,使用統一執行緒庫
  • 各業務線需要移除自己的執行緒庫使用統一的執行緒庫

基礎庫如何使用執行緒

  • 直接依賴執行緒庫
  • 缺點:執行緒庫更新可能會導致基礎庫也跟著更新

基礎庫優雅使用執行緒

  • 基礎庫內部暴露API:setExecutor
  • 初始化的時候注入統一的執行緒庫

舉個栗子:

比如這裡有一個紀錄檔工具類,我們將它作為應用的紀錄檔基礎庫,假設它內部有一些非同步操作,原始的情況下是它自己內部實現的,然後現在在它內部對外暴露一個API,如果外部注入了一個ExecutorService,那麼我們就使用外部注入的這個,如果外部沒有注入,那就使用它預設的,程式碼如下所示:

public class LogUtils {
    private static ExecutorService mExecutorService;
    public static void setExecutor(ExecutorService executorService){
        mExecutorService = executorService;
    }
    public static final String TAG = "Jarchie";
    public static void i(String msg){
        if(Utils.isMainProcess(BaseApp.getApplication())){
            Log.i(TAG,msg);
        }
        // 非同步操作
        if(mExecutorService != null){
            mExecutorService.execute(() -> {
                ...
            });
        }else {
            //使用原有的
            ...
        }
    }
}

統一執行緒庫

  • 區分任務型別:IO密集型、CPU密集型
  • IO密集型任務不消耗CPU,核心池可以很大(網路請求、IO讀寫等)
  • CPU密集型任務:核心池大小和CPU核心數相關(如果並行數超過核心數會導致CPU頻繁切換,降低執行效率)

舉個栗子:根據上面的說明,可以做如下的設定:

    //獲取CPU的核心數
    private int CPUCOUNT = Runtime.getRuntime().availableProcessors();
    //cpu執行緒池,核心數大小需要和cpu核心數相關聯,這裡簡單的將它們保持一致了
    private ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(CPUCOUNT, CPUCOUNT,
            30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
    //IO執行緒池,核心數64,這個數量可以針對自身專案再確定
    private ThreadPoolExecutor iOExecutor = new ThreadPoolExecutor(64, 64,
            30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
    //這裡面使用了一個count作為標記
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, "ThreadPoolUtils #" + mCount.getAndIncrement());
        }
    };

然後在你實際專案中需要區分具體的任務型別,針對性的選擇相應的執行緒池進行使用。 以上就是對於Android執行緒優化方面的總結了,今天的內容還好不算多,覺得有用的朋友可以看看。

以上就是Android 執行緒優化知識點學習的詳細內容,更多關於Android 執行緒優化的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com