Android性能優化篇之多線程并發優化

image

引言

1. Android性能優化篇之內存優化--內存泄漏

2.Android性能優化篇之內存優化--內存優化分析工具

3.Android性能優化篇之UI渲染性能優化

4.Android性能優化篇之計算性能優化

5.Android性能優化篇之電量優化(1)——電量消耗分析

6.Android性能優化篇之電量優化(2)

7.Android性能優化篇之網絡優化

8.Android性能優化篇之Bitmap優化

9.Android性能優化篇之圖片壓縮優化

10.Android性能優化篇之多線程并發優化

11.Android性能優化篇之數據傳輸效率優化

12.Android性能優化篇之程序啟動時間性能優化

13.Android性能優化篇之安裝包性能優化

14.Android性能優化篇之服務優化

介紹

在程序開發的實踐當中,為了讓程序表現得更加流暢,我們肯定會需要使用到多線程來提升程序的并發執行性能。但是編寫多線程并發的代碼一直以來都是一個相對棘手的問題,所以想要獲得更佳的程序性能,我們非常有必要掌握多線程并發編程的基礎技能。

一.Thread 使用

在講解多線程之前,我們先來講解Thread使用幾個需要注意的點:

1.Thread 中斷
常用的有兩種方式:
(1).通過拋出InterruptedException來中斷線程
    public  static  class  MyThread extends Thread{
        private  int count=0;
        @Override
        public void run() {
            super.run();
            try{
                while(true){
                        count++;
                        System.out.println("count value:"+count);
                        if (this.interrupted() || this.isInterrupted()){
                            System.out.println("check interrupted show!");
                            throw new InterruptedException();
                        }
                }
            }catch ( InterruptedException e) {
                System.out.println("thread is stop!");
                e.printStackTrace();
            }
        }
        
    } 
(2).通過變量來中斷(常用)
    public  static  class  CustomThread extends Thread{
        private  int count=0;
        private boolean isCancel = false;
        @Override
        public void run() {
            super.run();
            while(!isCancel){
                    count++;
                    System.out.println("count value:"+count);
            }
        }
        
        public synchronized void cancel(){
            isCancel = true;
        }
    } 
2.同步

我們分變量同步和代碼塊同步兩個方面來講解

(1).變量同步
使用volatile關鍵字
    /**
     * 主內存和線程內存緩存進行同步
     */
    volatile int val = 5;
    public int getVal() {
        return val;
    }
    public void setVal(int val) {
        this.val = val;
    }
使用synchronized關鍵字
    int val2 = 5;
    /**
     * 使用一個motinor來監聽(實現資源由一個線程進行操作)
     * 主內存和線程內存緩存進行同步
     * @return
     */
    public synchronized int getVal2() {
        return val2;
    }
    public synchronized int setVal2(int val) {
        this.val2 = val;
    }
使用關鍵字AtomicXXXXX
    AtomicInteger mAtomicValue = new  AtomicInteger(0);
    public void setAtomicValue(int value){
        mAtomicValue.getAndSet(value);
    }
    public int getAtomicValue(){
        return mAtomicValue.get();
    }
(2).代碼塊同步

代碼塊同步分樂觀鎖和悲觀鎖來講解

使用悲觀鎖時,其他線程等待,進入睡眠,頻繁切換任務,消耗cpu資源
    synchronized (this) {
        .....   
    }
使用樂觀鎖時,失敗重試,避免任務重復切換,減少cpu消耗
    ReentrantLock lock = new  ReentrantLock();
    lock.lock();
    ......
    lock.unlock();

Thread注意點就講到這里,下面讓我們進入今天的主題,多線程并發優化。

二.Android Threading

android中很多操作需要在主線程中執行,比如UI的操作,點擊事件等等,但是如果主線程操作太多,占有的執行時間過長就會出現前面我們說的卡頓現象:


image1.jpg

為了減輕主線程操作過多,避免出現卡頓的現象,我們把一些操作復雜的消耗時間長的任務放到線程池中去執行。下面我們就來介紹android中幾種線程的類。

1.AsyncTask

為UI線程與工作線程之間進行快速的切換提供一種簡單便捷的機制。適用于當下立即需要啟動,但是異步執行的生命周期短暫的使用場景。
它提供了一種簡便的異步處理機制,但是它又同時引入了一些令人厭惡的麻煩。一旦對AsyncTask使用不當,很可能對程序的性能帶來負面影響,同時還可能導致內存泄露。(關于內存泄漏在上面已經講過)

使用AsyncTask需要注意的問題?
(1).在AsyncTask中所有的任務都是被線性調度執行的,他們處在同一個任務隊列當中,按順序逐個執行。一旦有任務執行時間過長,隊列中其他任務就會阻塞。
image3.jpg

對于上面的問題,我們可以使用AsyncTask.executeOnExecutor()讓AsyncTask變成并發調度。

(2).AsyncTask對正在執行的任務不具備取消的功能,所以我們要在任務代碼中添加取消的邏輯(和上面Thread類似)
(3).AsyncTask使用不當會導致內存泄漏(可以參考內存泄漏一章)
2.HandlerThread

為某些回調方法或者等待某些任務的執行設置一個專屬的線程,并提供線程任務的調度機制。
先來了解下Looper,Handler,MessageQueue
Looper: 能夠確保線程持續存活并且可以不斷的從任務隊列中獲取任務并進行執行。
Handler: 能夠幫助實現隊列任務的管理,不僅僅能夠把任務插入到隊列的頭部,尾部,還可以按照一定的時間延遲來確保任務從隊列中能夠來得及被取消掉。
MessageQueue: 使用Intent,Message,Runnable作為任務的載體在不同的線程之間進行傳遞。
把上面三個組件打包到一起進行協作,這就是HandlerThread


image2.jpg

我們先來看下源碼:

    public class HandlerThread extends Thread {
        public HandlerThread(String name, int priority) {
            super(name);
            mPriority = priority;
        }

        @Override
        public void run() {
            mTid = Process.myTid();
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                notifyAll();
            }
            Process.setThreadPriority(mPriority);
            onLooperPrepared();
            Looper.loop();
            mTid = -1;
        }

        public Looper getLooper() {
            if (!isAlive()) {
                return null;
            }
            // If the thread has been started, wait until the looper has been created.
            synchronized (this) {
                while (isAlive() && mLooper == null) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            return mLooper;
        }
    }
從上面的源碼發現,HandlerThread其實就是在線程中維持一個消息循環隊列。下面我們看下使用:
    HandlerThread mHanderThread = new HandlerThread("hanlderThreadTest", Process.THREAD_PRIORITY_BACKGROUND);
    mHanderThread.run();
    Looper mHanderThreadLooper = mHanderThread.getLooper();

    Handler mHandler = new Handler(mHanderThreadLooper){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //子線程中執行
            ...
        }
    };
    //發送消息
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            ...
        }
    });  
3.IntentService

適合于執行由UI觸發的后臺Service任務,并可以把后臺任務執行的情況通過一定的機制反饋給UI。
默認的Service是執行在主線程的,可是通常情況下,這很容易影響到程序的繪制性能(搶占了主線程的資源)。除了前面介紹過的AsyncTask與HandlerThread,我們還可以選擇使用IntentService來實現異步操作。IntentService繼承自普通Service同時又在內部創建了一個HandlerThread,在onHandlerIntent()的回調里面處理扔到IntentService的任務。所以IntentService就不僅僅具備了異步線程的特性,還同時保留了Service不受主頁面生命周期影響的特點。


image5.jpg
使用IntentService需要特別注意的點:
(1).因為IntentService內置的是HandlerThread作為異步線程,所以每一個交給IntentService的任務都將以隊列的方式逐個被執行到,一旦隊列中有某個任務執行時間過長,那么就會導致后續的任務都會被延遲處理。
(2).通常使用到IntentService的時候,我們會結合使用BroadcastReceiver把工作線程的任務執行結果返回給主UI線程。使用廣播容易引起性能問題,我們可以使用LocalBroadcastManager來發送只在程序內部傳遞的廣播,從而提升廣播的性能。我們也可以使用runOnUiThread()快速回調到主UI線程。
(3).包含正在運行的IntentService的程序相比起純粹的后臺程序更不容易被系統殺死,該程序的優先級是介于前臺程序與純后臺程序之間的。
4.Loader

對于3.0后ContentProvider中的耗時操作,推薦使用Loader異步加載數據機制。相對其他加載機制,Loader有那些優點呢?

  • 提供異步加載數據機制
  • 對數據源變化進行監聽,實時更新數據
  • 在Activity配置發生變化(如橫豎屏切換)時不用重復加載數據
  • 適用于任何Activity和Fragment
    下面我們來看下Loader的具體使用:
    我們以獲得手機中所有的圖片為例:
    getLoaderManager().initLoader(LOADER_TYPE, null, mLoaderCallback);
    LoaderManager.LoaderCallbacks<Cursor> mLoaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() {
        private final String[] IMAGE_COLUMNS={
                MediaStore.Images.Media.DATA,//圖片路徑
                MediaStore.Images.Media.DISPLAY_NAME,//顯示的名字
                MediaStore.Images.Media.DATE_ADDED,//添加時間
                MediaStore.Images.Media.MIME_TYPE,//圖片擴展類型
                MediaStore.Images.Media.SIZE,//圖片大小
                MediaStore.Images.Media._ID,//圖片id
        };

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            toggleShowLoading(true,getString(R.string.common_loading));

            CursorLoader cursorLoader = new CursorLoader(ImageSelectActivity.this,                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,IMAGE_COLUMNS,
                    IMAGE_COLUMNS[4] + " > 0 AND "+IMAGE_COLUMNS[3] + " =? OR " +IMAGE_COLUMNS[3] + " =? ",
                    new String[]{"image/jpeg","image/png"},IMAGE_COLUMNS[2] + " DESC");
            return cursorLoader;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            if(data != null && data.getCount() > 0){
                ArrayList<String> imageList = new ArrayList<>();

                if(mShowCamera){
                    imageList.add("");
                }
                while (data.moveToNext()){
                    String path = data.getString(data.getColumnIndexOrThrow(IMAGE_COLUMNS[0]));
                    imageList.add(path);
                    Log.e("ImageSelect", "IIIIIIIIIIIIIIIIIIII=====>"+path);
                }
                //顯示數據
                showListData(imageList);
                toggleShowLoading(false,getString(R.string.common_loading));
            }
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {  
        }   

onCreateLoader() 實例化并返回一個新創建給定ID的Loader對象
onLoadFinished() 當創建好的Loader完成了數據的load之后回調此方法
onLoaderReset() 當創建好的Loader被reset時調用此方法,這樣保證它的數據無效
LoaderManager會對查詢的操作進行緩存,只要對應Cursor上的數據源沒有發生變化,在配置信息發生改變的時候(例如屏幕的旋轉),Loader可以直接把緩存的數據回調到onLoadFinished(),從而避免重新查詢數據。另外系統會在Loader不再需要使用到的時候(例如使用Back按鈕退出當前頁面)回調onLoaderReset()方法,我們可以在這里做數據的清除等等操作。

5.ThreadPool

把任務分解成不同的單元,分發到各個不同的線程上,進行同時并發處理。
線程池適合用在把任務進行分解,并發進行執行的場景。
系統提供ThreadPoolExecutor幫助類來幫助我們簡化實現線程池。


image4.jpg

使用線程池需要特別注意同時并發線程數量的控制,理論上來說,我們可以設置任意你想要的并發數量,但是這樣做非常的不好。因為CPU只能同時執行固定數量的線程數,一旦同時并發的線程數量超過CPU能夠同時執行的閾值,CPU就需要花費精力來判斷到底哪些線程的優先級比較高,需要在不同的線程之間進行調度切換。
一旦同時并發的線程數量達到一定的量級,這個時候CPU在不同線程之間進行調度的時間就可能過長,反而導致性能嚴重下降。另外需要關注的一點是,每開一個新的線程,都會耗費至少64K+的內存。為了能夠方便的對線程數量進行控制,ThreadPoolExecutor為我們提供了初始化的并發線程數量,以及最大的并發數量進行設置。

    /**
     * 核心線程數
     * 最大線程數
     * ?;顣r間
     * 時間單位
     * 任務隊列
     * 線程工廠
     */
    threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            linkedBlockingQueue, sThreadFactory);
    threadPoolExecutor.execute(runnable);
我們知道系統還提供了Executors類中幾種線程池,下面我們來看下這些線程池的缺點:

newFixedThreadPool 和 newSingleThreadExecutor:主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至 OOM。
newCachedThreadPool 和 newScheduledThreadPool:主要問題是線程數最大數是 Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至 OOM

我們看到這些線程池但是有缺點的,所以具體使用那種方式實現要根據我們的需求來選擇。

如果想要避開上面的問題,可以參考OKHttp中線程池的實現,OKHttp中隊線程調度又封裝了一層,使用安全且方便,有興趣的可以去看看源碼。

三.線程優先級

Android系統會根據當前運行的可見的程序和不可見的后臺程序對線程進行歸類,劃分為forground的那部分線程會大致占用掉CPU的90%左右的時間片,background的那部分線程就總共只能分享到5%-10%左右的時間片。之所以設計成這樣是因為forground的程序本身的優先級就更高,理應得到更多的執行時間。


image6.jpg

默認情況下,新創建的線程的優先級默認和創建它的母線程保持一致。如果主UI線程創建出了幾十個工作線程,這些工作線程的優先級就默認和主線程保持一致了,為了不讓新創建的工作線程和主線程搶占CPU資源,需要把這些線程的優先級進行降低處理,這樣才能給幫組CPU識別主次,提高主線程所能得到的系統資源。

在Android系統里面,我們可以通過android.os.Process.setThreadPriority(int)設置線程的優先級,參數范圍從-20到24,數值越小優先級越高。Android系統還為我們提供了以下的一些預設值,我們可以通過給不同的工作線程設置不同數值的優先級來達到更細粒度的控制。


image7.jpg
大多數情況下,新創建的線程優先級會被設置為默認的0,主線程設置為0的時候,新創建的線程還可以利用THREAD_PRIORITY_LESS_FAVORABLE或者THREAD_PRIORITY_MORE_FAVORABLE來控制線程的優先級。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容