Android 線程優(yōu)化知識點學(xué)習(xí)
前言
在實際項目開發(fā)中會頻繁的用到線程,線程使用起來是很簡單,但是濫用線程會帶來性能問題, 比如啟動一個線程至少 占用16kb的內(nèi)存、線程過多會導(dǎo)致cpu的頻繁切換而cpu切換成本是很高的、消耗大量用戶電量等問題, 所以應(yīng)該讓app的線程數(shù)保持在合理水平,這是性能優(yōu)化中很重要的一部分。本文對線程優(yōu)化方面的知識做了一個全面總結(jié),主要內(nèi)容如下:
一、線程調(diào)度原理解析
線程調(diào)度的原理
在任意時刻,CPU 只能執(zhí)行一條機器指令,每個線程只有獲得了 CPU 的使用權(quán)之后才能執(zhí)行指令,也就是說 在任意時刻,只有一個線程占用 CPU,處于運行狀態(tài)。而我們平常所說的 多線程并發(fā)運行,實際上說的是多個線程輪流獲取 CPU 的使用權(quán),然后分別執(zhí)行各自的任務(wù)。其實在可運行池當中有多個處于就緒狀態(tài)的線程在等待 CPU,而 JVM 負責線程調(diào)度,按照特定機制為多個線程分配 CPU 使用權(quán)。
上面的描述提到了三個主要信息:
- 在任意時刻,只有一個線程占用 CPU,處于運行狀態(tài)
- 多線程并發(fā)運行,實際上說的是多個線程輪流獲取 CPU 的使用權(quán)
- JVM 負責線程調(diào)度,按照特定機制為多個線程分配 CPU 使用權(quán)
線程調(diào)度模型
線程調(diào)度模型可以分為兩類,分別是 分時調(diào)度模型 和 搶占式調(diào)度模型。
- 分時調(diào)度模型:讓所有線程輪流獲取 CPU 的使用權(quán),而且均分每個線程占用 CPU 的時間片,這種方式非常公平
- 搶占式調(diào)度模型:JVM 使用的是搶占式調(diào)度模型,讓優(yōu)先級高的線程優(yōu)先獲取到 CPU 的使用權(quán),如果在可運行池當中的線程優(yōu)先級都一樣,那就隨機選取一個
Android 的線程調(diào)度
Android 的線程調(diào)度從兩個因素決定,一個是 nice
值(即線程優(yōu)先級),一個是 cgroup
(即線程調(diào)度策略)。
對于 nice 值來說,它首先是在 Process
中定義的,值越小,進程優(yōu)先級越高,默認值是 THREAD_PRIORITY_DEFAULT = 0
,主線程的優(yōu)先級也是這個值。修改 nice 值只需要在對應(yīng)的線程下設(shè)置即可:
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 * <var>tid</var> 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)先級比較低,但是數(shù)量比較多,這10個后臺線程對 CPU 的消耗量是可以影響到前臺線程的性能的。所以 Android 需要一種機制來處理這種情況,也就是 cgroup。
Android 借鑒了 Linux 的 cgroup 來執(zhí)行 更嚴格的前臺和后臺調(diào)度策略,后臺優(yōu)先級的線程會被隱式的移動到后臺 group,而其他 group 的線程如果處于工作狀態(tài),那么后臺這些線程它們將會被限制,只有很小的幾率能夠利用 CPU。
這種分離的調(diào)度策略既允許了后臺線程來執(zhí)行一些任務(wù),同時又不會對用戶可見的前臺線程造成很大的影響,讓前臺線程有更多的 CPU。
或許你會有疑問:哪些線程會被移到后臺 group?
- 第一種就是那些 手動設(shè)置了優(yōu)先級比較低的線程
- 第二種就是 不在前臺運行的那些應(yīng)用程序的線程
線程調(diào)度小結(jié)
- 線程過多會導(dǎo)致 CPU 頻繁切換,降低線程運行效率。 在前面講解啟動優(yōu)化的時候有強調(diào)要充足的利用線程比如異步啟動任務(wù),但是線程也不能無限制的使用
- 正確認識任務(wù)重要性決定哪種優(yōu)先級。 一般情況下線程工作量和優(yōu)先級是成反比,比如線程的工作量越大,所做的工作沒那么重要,那這個線程的優(yōu)先級應(yīng)該越低
- 線程的優(yōu)先級具有繼承性。 比如在 A 線程創(chuàng)建了 B 線程,在我們沒有指定線程優(yōu)先級的情況下,B 線程的優(yōu)先級是和 A 一樣的。所以我們在 UI 線程中創(chuàng)建線程,線程的優(yōu)先級是和 UI 線程一樣的,這就會導(dǎo)致 UI 線程搶占 CPU 時間片的概率會變少
二、Android 異步方式匯總
Thread
使用 Thread
創(chuàng)建線程是最簡單、常見的異步方式,但在實際項目中,它也就只有這個優(yōu)點了,并不推薦直接使用 Thread 創(chuàng)建線程,主要有以下幾點原因:
- 不易復(fù)用,頻繁創(chuàng)建及銷毀開銷大
- 復(fù)雜場景不易使用
HandlerThread
是 Android 提供的一個自帶消息循環(huán)的線程,它內(nèi)部使用 串行的方式執(zhí)行任務(wù),比較 適合長時間運行,不斷從隊列中獲取任務(wù)的場景。
IntentService
繼承了 Android Service
組件,內(nèi)部創(chuàng)建了 HandlerThread,相比 Service 是在主線程執(zhí)行,IntentService 是 在子線程異步執(zhí)行不占用主線程,而且 優(yōu)先級比較高,不易被系統(tǒng) kill。
AsyncTask
AsyncTask
是 Android 提供的工具類,內(nèi)部的實現(xiàn)是使用了線程池,它比較大的好處是無需自己處理線程切換,但需要注意 AsyncTask 不同版本執(zhí)行方式不一致的問題。
線程池
java 提供了線程池,在實際項目中比較推薦使用線程池的方式實現(xiàn)異步任務(wù),它主要有以下優(yōu)點:
- 易復(fù)用,減少線程頻繁創(chuàng)建、銷毀的時間
- 功能強大:定時、任務(wù)隊列、并發(fā)數(shù)控制等,java 提供了
Executors
工具類可以很方便的創(chuàng)建一個線程池,也可以自己定制線程池
RxJava
RxJava
由強大的 Scheduler
集合提供,內(nèi)部實際也是使用的線程池,它封裝的非常完善,可以根據(jù)任務(wù)類型的不同指定使用不同的線程池,比如 IO 密集型的任務(wù)可以指定 Schedulers.IO
,CPU 密集型任務(wù)可以指定 Schedulers.Computation
。
Single.just(xxx) .subscribeOn(Schedulers.IO) // 指定工作線程類型為 IO 密集型 .observeOn(AndroidSchedulers.mainThread()) // 指定下游接收所在線程 .subscribe();
三、Android線程優(yōu)化實戰(zhàn)
線程使用準則
- 嚴禁使用直接new Thread()的方式
- 提供基礎(chǔ)線程池供各個業(yè)務(wù)線使用: 避免各個業(yè)務(wù)線各自維護一套線程池,導(dǎo)致線程數(shù)過多
- 根據(jù)任務(wù)類型選擇合適的異步方式: 比如優(yōu)先級低且長時間執(zhí)行可以使用Handler Thread,再比如:有一個任務(wù)需要定時執(zhí)行,使用線程池更適合
- 創(chuàng)建線程必須命名: 方便定位線程歸屬于哪一個業(yè)務(wù)方,在線程運行期可以使用Thread.currentThread().setName修改名字
- 關(guān)鍵異步任務(wù)監(jiān)控: 異步不等于不耗時,如果一個任務(wù)在主線程需要耗費500ms,那么它在異步任務(wù)中至少需要500ms,因為異步任務(wù)中優(yōu)先級較低,耗費時間很可能會高于500ms,所以這里可以使用AOP的方式來做監(jiān)控,并且結(jié)合所在的業(yè)務(wù)場景,根據(jù)監(jiān)控結(jié)果來適時的做一些相對應(yīng)的調(diào)整
- 重視優(yōu)先級設(shè)置: 使用Process.setThreadPriority();設(shè)置,并且可以設(shè)置多次
線程池優(yōu)化實戰(zhàn)
接下來針對線程池的使用來做一個簡單的實踐,還是打開我們之前的項目,這里說一下每次實踐的代碼都是基于第一篇啟動優(yōu)化的那個案例上寫的。
首先新建一個包async,然后在包中創(chuàng)建一個類ThreadPoolUtils,這里我們創(chuàng)建可重用且固定線程數(shù)的線程池,核心數(shù)為5,并且對外暴露一個get方法,然后我們可以在任何地方都能獲取到這個全局的線程池:
public class ThreadPoolUtils { //創(chuàng)建定長線程池,核心數(shù)為5 private static ExecutorService mService = Executors.newFixedThreadPool(5, new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { Thread thread = new Thread(runnable,"ThreadPoolUtils");//設(shè)置線程名 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //設(shè)置線程優(yōu)先級 return thread; } }); //獲取全局的線程池 public static ExecutorService getService(){ return mService; } }
然后使用的時候就可以在你需要的地方直接調(diào)用了,并且你在使用的時候還可以修改線程的優(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)建者
當你的項目做的越來越大的時候一般情況下線程都會變的非常多,最好是能夠?qū)φw的線程數(shù)進行收斂,那么問題來了,如何知道某個線程是在哪里創(chuàng)建的呢?不僅僅是你自己的項目源碼,你依賴的第三方庫、aar中都有線程的創(chuàng)建,如果單靠人眼review代碼的方式,工作量很大而且你還不一定能找的全。
并且你這次優(yōu)化完了線程數(shù),你還要考慮其他人新加的線程是否合理,所以就需要能夠建立一套很好的監(jiān)控預(yù)防手段。然后針對這些情況來做一個解決方案的總結(jié)分析,主要思想就是以下兩點:
- 創(chuàng)建線程的位置獲取堆棧
- 所有的異步方式,都會走到new Thread
解決方案:
- 特別適合
Hook
手段 - 找Hook點:構(gòu)造函數(shù)或者特定方法
- Thread的構(gòu)造函數(shù)
可以在構(gòu)造函數(shù)中加上自己的邏輯,獲取當前的調(diào)用棧信息,拿到調(diào)用棧信息之后,就可以分析看出某個線程是否使用的是統(tǒng)一的線程池,也可以知道某個線程具體屬于哪個業(yè)務(wù)方。
Epic實戰(zhàn)
Epic簡介
- Epic是一個虛擬機層面、以Java Method為粒度的運行時Hook框架
- 支持Android4.0-10.0(我的手機上程序出現(xiàn)了閃退,后來查找原因發(fā)現(xiàn)這個庫開源版本一些高版本手機好像不支持)
Epic使用
- implementation 'me.weishu:epic:0.6.0'
- 繼承XC_MethodHook,實現(xiàn)相應(yīng)邏輯
- 注入Hook:DexposedBridge.findAndHookMethod
代碼中使用
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); //Hook Thread類的構(gòu)造函數(shù),兩個參數(shù):需要Hook的類,MethodHook的回調(diào) DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() { //afterHookedMethod是Hook此方法之后給我們的回調(diào) @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); //Hook完成之后會回調(diào)到這里 //實現(xiàn)自己的邏輯,param.thisObject可以拿到線程對象 Thread thread = (Thread) param.thisObject; //Log.getStackTraceString打印當前的調(diào)用棧信息 Log.i(thread.getName() + "stack", Log.getStackTraceString(new Throwable())); } }); }
如果你的手機支持的話,這個時候運行程序應(yīng)該就可以看到線程打印出來的堆棧信息了
五、優(yōu)雅實現(xiàn)線程收斂
線程收斂常規(guī)方案
- 根據(jù)線程創(chuàng)建堆??剂亢侠硇?,使用統(tǒng)一線程庫
- 各業(yè)務(wù)線需要移除自己的線程庫使用統(tǒng)一的線程庫
基礎(chǔ)庫如何使用線程
- 直接依賴線程庫
- 缺點:線程庫更新可能會導(dǎo)致基礎(chǔ)庫也跟著更新
基礎(chǔ)庫優(yōu)雅使用線程
- 基礎(chǔ)庫內(nèi)部暴露API:setExecutor
- 初始化的時候注入統(tǒng)一的線程庫
舉個栗子:
比如這里有一個日志工具類,我們將它作為應(yīng)用的日志基礎(chǔ)庫,假設(shè)它內(nèi)部有一些異步操作,原始的情況下是它自己內(nèi)部實現(xiàn)的,然后現(xiàn)在在它內(nèi)部對外暴露一個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ū)分任務(wù)類型:IO密集型、CPU密集型
- IO密集型任務(wù)不消耗CPU,核心池可以很大(網(wǎng)絡(luò)請求、IO讀寫等)
- CPU密集型任務(wù):核心池大小和CPU核心數(shù)相關(guān)(如果并發(fā)數(shù)超過核心數(shù)會導(dǎo)致CPU頻繁切換,降低執(zhí)行效率)
舉個栗子:根據(jù)上面的說明,可以做如下的設(shè)置:
//獲取CPU的核心數(shù) private int CPUCOUNT = Runtime.getRuntime().availableProcessors(); //cpu線程池,核心數(shù)大小需要和cpu核心數(shù)相關(guān)聯(lián),這里簡單的將它們保持一致了 private ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(CPUCOUNT, CPUCOUNT, 30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory); //IO線程池,核心數(shù)64,這個數(shù)量可以針對自身項目再確定 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()); } };
然后在你實際項目中需要區(qū)分具體的任務(wù)類型,針對性的選擇相應(yīng)的線程池進行使用。 以上就是對于Android線程優(yōu)化方面的總結(jié)了,今天的內(nèi)容還好不算多,覺得有用的朋友可以看看。
以上就是Android 線程優(yōu)化知識點學(xué)習(xí)的詳細內(nèi)容,更多關(guān)于Android 線程優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android App中使用RatingBar實現(xiàn)星級打分功能的教程
這篇文章主要介紹了Android App中使用RatingBar實現(xiàn)星級打分功能的教程,文中舉了一個使用SeekBar與RatingBar制作的應(yīng)用內(nèi)打分條的功能,非常簡單,需要的朋友可以參考下2016-04-04Flutter定時器、倒計時的快速上手及實戰(zhàn)講解
這篇文章主要給大家介紹了關(guān)于Flutter定時器、倒計時的快速上手及實戰(zhàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學(xué)習(xí)或者使用Flutter具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06Android Studio報:“Attribute application@theme or @ icon ”問題的解
這篇文章主要給大家介紹了關(guān)于Android Studio報:“Attribute application@theme or @ icon ”問題的解決方法,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12Android 自定義TextView去除paddingTop和paddingBottom
這篇文章主要介紹了Android 自定義TextView去除paddingTop和paddingBottom的相關(guān)資料,這里提供實例來幫助大家實現(xiàn)這樣的功能,需要的朋友可以參考下2017-09-09