溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android線程優(yōu)化知識點有哪些

發(fā)布時間:2022-08-12 10:25:40 來源:億速云 閱讀:122 作者:iii 欄目:開發(fā)技術

這篇文章主要講解了“Android線程優(yōu)化知識點有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Android線程優(yōu)化知識點有哪些”吧!

一、線程調度原理解析

線程調度的原理

在任意時刻,CPU 只能執(zhí)行一條機器指令,每個線程只有獲得了 CPU 的使用權之后才能執(zhí)行指令,也就是說 在任意時刻,只有一個線程占用 CPU,處于運行狀態(tài)。而我們平常所說的 多線程并發(fā)運行,實際上說的是多個線程輪流獲取 CPU 的使用權,然后分別執(zhí)行各自的任務。其實在可運行池當中有多個處于就緒狀態(tài)的線程在等待 CPU,而 JVM 負責線程調度,按照特定機制為多個線程分配 CPU 使用權。

上面的描述提到了三個主要信息:

  • 在任意時刻,只有一個線程占用 CPU,處于運行狀態(tài)

  • 多線程并發(fā)運行,實際上說的是多個線程輪流獲取 CPU 的使用權

  • JVM 負責線程調度,按照特定機制為多個線程分配 CPU 使用權

線程調度模型

線程調度模型可以分為兩類,分別是 分時調度模型 和 搶占式調度模型。

  • 分時調度模型:讓所有線程輪流獲取 CPU 的使用權,而且均分每個線程占用 CPU 的時間片,這種方式非常公平

  • 搶占式調度模型:JVM 使用的是搶占式調度模型,讓優(yōu)先級高的線程優(yōu)先獲取到 CPU 的使用權,如果在可運行池當中的線程優(yōu)先級都一樣,那就隨機選取一個

Android 的線程調度

Android 的線程調度從兩個因素決定,一個是 nice 值(即線程優(yōu)先級),一個是 cgroup(即線程調度策略)。

對于 nice 值來說,它首先是在 Process 中定義的,值越小,進程優(yōu)先級越高,默認值是 THREAD_PRIORITY_DEFAULT = 0,主線程的優(yōu)先級也是這個值。修改 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 值它還有其他的優(yōu)先級可選:

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個后臺線程,雖然后臺線程的優(yōu)先級比較低,但是數量比較多,這10個后臺線程對 CPU 的消耗量是可以影響到前臺線程的性能的。所以 Android 需要一種機制來處理這種情況,也就是 cgroup。

Android 借鑒了 Linux 的 cgroup 來執(zhí)行 更嚴格的前臺和后臺調度策略,后臺優(yōu)先級的線程會被隱式的移動到后臺 group,而其他 group 的線程如果處于工作狀態(tài),那么后臺這些線程它們將會被限制,只有很小的幾率能夠利用 CPU。

這種分離的調度策略既允許了后臺線程來執(zhí)行一些任務,同時又不會對用戶可見的前臺線程造成很大的影響,讓前臺線程有更多的 CPU。

或許你會有疑問:哪些線程會被移到后臺 group?

  • 第一種就是那些 手動設置了優(yōu)先級比較低的線程

  • 第二種就是 不在前臺運行的那些應用程序的線程

線程調度小結

  • 線程過多會導致 CPU 頻繁切換,降低線程運行效率。 在前面講解啟動優(yōu)化的時候有強調要充足的利用線程比如異步啟動任務,但是線程也不能無限制的使用

  • 正確認識任務重要性決定哪種優(yōu)先級。 一般情況下線程工作量和優(yōu)先級是成反比,比如線程的工作量越大,所做的工作沒那么重要,那這個線程的優(yōu)先級應該越低

  • 線程的優(yōu)先級具有繼承性。 比如在 A 線程創(chuàng)建了 B 線程,在我們沒有指定線程優(yōu)先級的情況下,B 線程的優(yōu)先級是和 A 一樣的。所以我們在 UI 線程中創(chuàng)建線程,線程的優(yōu)先級是和 UI 線程一樣的,這就會導致 UI 線程搶占 CPU 時間片的概率會變少

二、Android 異步方式匯總

Thread

使用 Thread 創(chuàng)建線程是最簡單、常見的異步方式,但在實際項目中,它也就只有這個優(yōu)點了,并不推薦直接使用 Thread 創(chuàng)建線程,主要有以下幾點原因:

  • 不易復用,頻繁創(chuàng)建及銷毀開銷大

  • 復雜場景不易使用

HandlerThread

是 Android 提供的一個自帶消息循環(huán)的線程,它內部使用 串行的方式執(zhí)行任務,比較 適合長時間運行,不斷從隊列中獲取任務的場景。

IntentService

繼承了 Android Service 組件,內部創(chuàng)建了 HandlerThread,相比 Service 是在主線程執(zhí)行,IntentService 是 在子線程異步執(zhí)行不占用主線程,而且 優(yōu)先級比較高,不易被系統(tǒng) kill。

AsyncTask

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

線程池

java 提供了線程池,在實際項目中比較推薦使用線程池的方式實現異步任務,它主要有以下優(yōu)點:

  • 易復用,減少線程頻繁創(chuàng)建、銷毀的時間

  • 功能強大:定時、任務隊列、并發(fā)數控制等,java 提供了 Executors 工具類可以很方便的創(chuàng)建一個線程池,也可以自己定制線程池

RxJava

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

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

三、Android線程優(yōu)化實戰(zhàn)

線程使用準則

  • 嚴禁使用直接new Thread()的方式

  • 提供基礎線程池供各個業(yè)務線使用: 避免各個業(yè)務線各自維護一套線程池,導致線程數過多

  • 根據任務類型選擇合適的異步方式: 比如優(yōu)先級低且長時間執(zhí)行可以使用Handler Thread,再比如:有一個任務需要定時執(zhí)行,使用線程池更適合

  • 創(chuàng)建線程必須命名: 方便定位線程歸屬于哪一個業(yè)務方,在線程運行期可以使用Thread.currentThread().setName修改名字

  • 關鍵異步任務監(jiān)控: 異步不等于不耗時,如果一個任務在主線程需要耗費500ms,那么它在異步任務中至少需要500ms,因為異步任務中優(yōu)先級較低,耗費時間很可能會高于500ms,所以這里可以使用AOP的方式來做監(jiān)控,并且結合所在的業(yè)務場景,根據監(jiān)控結果來適時的做一些相對應的調整

  • 重視優(yōu)先級設置: 使用Process.setThreadPriority();設置,并且可以設置多次

線程池優(yōu)化實戰(zhàn)

接下來針對線程池的使用來做一個簡單的實踐,還是打開我們之前的項目,這里說一下每次實踐的代碼都是基于第一篇啟動優(yōu)化的那個案例上寫的。

首先新建一個包async,然后在包中創(chuàng)建一個類ThreadPoolUtils,這里我們創(chuàng)建可重用且固定線程數的線程池,核心數為5,并且對外暴露一個get方法,然后我們可以在任何地方都能獲取到這個全局的線程池:

public class ThreadPoolUtils {
    //創(chuàng)建定長線程池,核心數為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); //設置線程優(yōu)先級
            return thread;
        }
    });
    //獲取全局的線程池
    public static ExecutorService getService(){
        return mService;
    }
}

然后使用的時候就可以在你需要的地方直接調用了,并且你在使用的時候還可以修改線程的優(yōu)先級以及線程名稱:

        //使用全局統(tǒng)一的線程池
        ThreadPoolUtils.getService().execute(new Runnable() {
            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); //修改線程優(yōu)先級
                String oldName = Thread.currentThread().getName();
                Thread.currentThread().setName("Jarchie"); //修改線程名稱
                Log.i("MainActivity","");
                Thread.currentThread().setName(oldName); //將原有名稱改回去
            }
        });

四、定位線程創(chuàng)建者

如何確定線程創(chuàng)建者

當你的項目做的越來越大的時候一般情況下線程都會變的非常多,最好是能夠對整體的線程數進行收斂,那么問題來了,如何知道某個線程是在哪里創(chuàng)建的呢?不僅僅是你自己的項目源碼,你依賴的第三方庫、aar中都有線程的創(chuàng)建,如果單靠人眼review代碼的方式,工作量很大而且你還不一定能找的全。

并且你這次優(yōu)化完了線程數,你還要考慮其他人新加的線程是否合理,所以就需要能夠建立一套很好的監(jiān)控預防手段。然后針對這些情況來做一個解決方案的總結分析,主要思想就是以下兩點:

  • 創(chuàng)建線程的位置獲取堆棧

  • 所有的異步方式,都會走到new Thread

解決方案:

  • 特別適合Hook手段

  • 找Hook點:構造函數或者特定方法

  • Thread的構造函數

可以在構造函數中加上自己的邏輯,獲取當前的調用棧信息,拿到調用棧信息之后,就可以分析看出某個線程是否使用的是統(tǒng)一的線程池,也可以知道某個線程具體屬于哪個業(yè)務方。

Epic實戰(zhàn)

Epic簡介

  • Epic是一個虛擬機層面、以Java Method為粒度的運行時Hook框架

  • 支持Android4.0-10.0(我的手機上程序出現了閃退,后來查找原因發(fā)現這個庫開源版本一些高版本手機好像不支持)

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()));
            }
        });
    }

如果你的手機支持的話,這個時候運行程序應該就可以看到線程打印出來的堆棧信息了

五、優(yōu)雅實現線程收斂

線程收斂常規(guī)方案

  • 根據線程創(chuàng)建堆棧考量合理性,使用統(tǒng)一線程庫

  • 各業(yè)務線需要移除自己的線程庫使用統(tǒng)一的線程庫

基礎庫如何使用線程

  • 直接依賴線程庫

  • 缺點:線程庫更新可能會導致基礎庫也跟著更新

基礎庫優(yōu)雅使用線程

  • 基礎庫內部暴露API:setExecutor

  • 初始化的時候注入統(tǒng)一的線程庫

舉個栗子:

比如這里有一個日志工具類,我們將它作為應用的日志基礎庫,假設它內部有一些異步操作,原始的情況下是它自己內部實現的,然后現在在它內部對外暴露一個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 {
            //使用原有的
            ...
        }
    }
}

統(tǒng)一線程庫

  • 區(qū)分任務類型:IO密集型、CPU密集型

  • IO密集型任務不消耗CPU,核心池可以很大(網絡請求、IO讀寫等)

  • CPU密集型任務:核心池大小和CPU核心數相關(如果并發(fā)數超過核心數會導致CPU頻繁切換,降低執(zhí)行效率)

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

    //獲取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線程優(yōu)化知識點有哪些”的內容了,經過本文的學習后,相信大家對Android線程優(yōu)化知識點有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI