Android線程優化你瞭解多少

目錄

寫在前面

一、Android線程調度原理解析

1.1、線程調度原理

1.2、線程調度模型

1.3、Android線程調度

二、Android異步方式

三、Android線程優化實戰

3.1、線程使用準則

3.2、線程池優化實戰

四、定位線程創建者

4.1、如何確定線程創建者

4.2、Epic實戰

五、優雅實現線程收斂

5.1、線程收斂常規方案

5.2、基礎庫如何使用線程

5.3、基礎庫優雅使用線程


寫在前面

各位小夥伴們早上好,端午節即將到來,提前恭祝大家“端午安康”!

在上一篇中我們說到了Android平臺卡頓優化的相關知識,還沒了解的可以先去了解一波哦——《你想要知道的android卡頓優化》,

今天咱們繼續Android性能優化專題的分析,來到了Android線程優化。

一、Android線程調度原理解析

1.1、線程調度原理

  • 在任意時刻,CPU只能執行一條機器指令,每個線程只有獲取到CPU的使用權之後纔可以執行指令,也就是說,在任意時刻,只有一個線程佔用CPU,處於運行狀態
  • 多線程併發:實際上是指多個線程輪流獲取CPU的使用權,分別執行各自的任務
  • JVM負責線程調度:在可運行池中實際是有多個處於就緒狀態的線程在等待CPU,JVM按照特定機制分配CPU使用權

1.2、線程調度模型

  • 分時調度模型:所有線程輪流獲取CPU使用權,平均分配每個線程佔用CPU的時間片
  • 搶佔式調度模型(JVM採用):優先讓可運行池中優先級較高的線程佔用CPU,如果優先級都一樣則隨機選取一個讓其佔用CPU。這裏需要注意,由於JVM的線程調度並不是分時調度,因此同時啓用多個線程之後並不能保證多個線程輪流獲取到均等的時間片,所以如果程序希望干預線程的調度過程,最簡單的方式就是給每個線程設定優先級

1.3、Android線程調度

①、nice值

  • Process中定義
  • 值越小,優先級越高
  • 默認是THREAD_PRIORITY_DEFAULT,值爲0

下面是android.os.Process類中定義的各個優先級:

②、cgroup

對於Android來說,只有nice值實際上並不能滿足所有場景,比如某個應用有一個前臺的UI線程,同時它還有10個後臺線程,雖然後臺線程的優先級比較低,但是數量較多,合起來這些後臺線程對CPU的消耗也會影響到前臺線程的性能,所以對於Android來說又引入了另外一套機制來處理這種特殊的情況——cgroup。

  • 更嚴格的羣組調度策略:後臺優先級的線程會被隱式的移到後臺group,當其他組的線程處於工作狀態,後臺group的線程會被限制,只有很小的機率能利用CPU,這種分離的策略,既允許了後臺線程能執行一些任務,同時也不會對用戶可見的前臺線程造成很大的影響
  • 保證前臺線程可以獲取到更多的CPU
  • 可能會被移到後臺group的線程:①、手動設置了優先級較低的線程;②、不在前臺運行的應用程序的線程

需要注意的問題

  • 線程過多會導致CPU頻繁切換,降低線程運行效率:異步不能無限制的使用
  • 正確認識任務重要性然後決定使用哪種優先級:一般情況下線程的優先級是和它所承擔的工作量成反比,即:工作量越大優先級越低,CPU空閒階段,線程的優先級對執行效率的影響並不明顯,但是如果CPU處於忙碌階段,線程頻繁調度會對CPU產生較大影響
  • 優先級具有繼承性:舉個栗子:在線程A中創建了線程B,如果沒有指定線程B的優先級,則B的優先級會默認繼承A的優先級,如果在UI線程中直接創建了一個子線程,實際上它倆的優先級是一樣的,如果UI線程去搶佔CPU的時間片概率會變小

二、Android異步方式

①、Thread:最簡單、常見的異步方式

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

②、Handler Thread:自帶消息循環的線程

  • 串行執行
  • 長時間運行,不斷從隊列中獲取任務

③、Intent Service:繼承自Service在內部創建Handler Thread

  • 異步,不佔用UI線程
  • 優先級較高,不會輕易被系統Kill掉

④、AsyncTask:Android提供的異步工具類,內部實現是基於線程池

  • 無需自己處理線程切換
  • 需注意版本不一致問題(早期版本api不一致,由於現在適配版本普遍提高了,所以這個問題可以忽略)

⑤、線程池:Java提供的線程池

  • 易複用,減少頻繁創建、銷燬的時間
  • 功能強大:定時機制、任務隊列、併發數控制等等

⑥、RxJava:由強大的Scheduler集合提供(這裏只看線程調度功能)

  • 不同類型的區分:IO、Computation

總結:

  • 推薦程度:由後向前依次降低
  • 正確場景選擇正確的方式

三、Android線程優化實戰

3.1、線程使用準則

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

3.2、線程池優化實戰

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

首先新建一個包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); //將原有名稱改回去
            }
        });

四、定位線程創建者

4.1、如何確定線程創建者

當你的項目做的越來越大的時候一般情況下線程都會變的非常多,最好是能夠對整體的線程數進行收斂,那麼問題來了,如何知道某個線程是在哪裏創建的呢?不僅僅是你自己的項目源碼,你依賴的第三方庫、aar中都有線程的創建,如果單靠人眼review代碼的方式,工作量很大而且你還不一定能找的全。並且你這次優化完了線程數,你還要考慮其他人新加的線程是否合理,所以就需要能夠建立一套很好的監控預防手段。然後針對這些情況來做一個解決方案的總結分析,主要思想就是以下兩點:

  • 創建線程的位置獲取堆棧
  • 所有的異步方式,都會走到new Thread

解決方案:

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

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

4.2、Epic實戰

Epic簡介

  • Epic是一個虛擬機層面、以Java Method爲粒度的運行時Hook框架
  • 支持Android4.0-10.0(我的手機上程序出現了閃退,後來查找原因發現這個庫開源版本一些高版本手機好像不支持)
  • https://github.com/tiann/epic

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

如果你的手機支持的話,這個時候運行程序應該就可以看到線程打印出來的堆棧信息了,我的手機不支持,所以就隨便扒了一張圖給大家了:

五、優雅實現線程收斂

5.1、線程收斂常規方案

  • 根據線程創建堆棧考量合理性,使用統一線程庫
  • 各業務線需要移除自己的線程庫使用統一的線程庫

5.2、基礎庫如何使用線程

  • 直接依賴線程庫
  • 缺點:線程庫更新可能會導致基礎庫也跟着更新

5.3、基礎庫優雅使用線程

  • 基礎庫內部暴露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線程優化方面的總結了,今天的內容還好不算多,覺得有用的朋友可以看看。

OK,廢話就不多說了,今天就先到這裏吧,各位下期再會!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章