1. 程式人生 > >5.UI執行緒和非UI執行緒的互動方式

5.UI執行緒和非UI執行緒的互動方式

  一般來說有三種方式:      1.Activity.unOnUiThread(Runnable)      如果當前執行緒是UI Thread,立馬執行action.run方法;否則將Runnable傳送到UI Thread的event 佇列中。     2. view.post(Runnable)      將action加入到UI thread 的message queue。      3.view.postDelayed(Runnable,long)同2一樣,經過long時間之後將runnable傳送給message queue。      5.1UI執行緒   當一個app啟動時,系統為該app建立一個執行緒,成為主執行緒
。這個主執行緒管理ui中的元件,包括分發事件重繪view等。因此也叫做UI Thread。我們還知道當預設情況下,系統並不為四大元件建立新的執行緒,都是執行在這個UI Thread之中,這種方式也稱為單執行緒模型。所以一旦app中出現了耗時的工作(訪問網路,寫檔案,讀寫資料庫等等)有可能導致thread阻塞,如果這時系統無法及時分發事件,對於使用者來說最直觀的感受就是卡住了,而這時使用者又不停的點選螢幕很有可能出現ANR。在設計app時要盡力避免出現ANR的出現,關於ANR請看5.2
     而且UI toolkit不是執行緒安全的,所以一定不能從子執行緒中操作ui,只能在ui thread中操作ui。對於android中的這種單執行緒模型有兩條規則
     1.不要阻塞UI thread           2.不要在非UI Thread中操作UI      如果在子執行緒中訪問ui執行緒中的view,會報錯:android.view.ViewRootImpl$CalledFromWrongThreadException:Onlythe original thread that created a view hierarchy can touch its views.      但是現實中需要進行耗時操作的場景又非常多,如果需要耗時操作,然後更新UI怎麼做呢(下載圖片然後更新imageview)?我們先來按照它的規則來指定自己的方案:
     先按照規則1:不在UI Thread中做耗時操作,那麼我們開啟一個子執行緒來處理耗時操作,然後再更新view      newThread(new Runnable(){                public void run(){                           //模擬耗時操作,比如上網下載圖片                          Bitmap b = loadBitmapFormWeb(); imageview.setBitmap(b);//在子執行緒中如果直接訪問UI執行緒,會報錯,不允許這麼做。 //這裡可以操作runOnUiThread,imageView.post(Runnable),imageView.postDelay();                          imageview.post(new Runnable(){  imageview.setImageBitmap(b);                          });                }      }).start();      但是這又不符合規則2,不能在非UI Thread中操作UI。這就很矛盾了,android系統在設計時已經考慮過這個問題,所以在android系統提供了幾種方式在非UI Thread中訪問UI執行緒:                      Activity.runOnUiThread,View.post(),View.postDelayed()。      這樣的話,耗時操作在子執行緒中執行,UI 更新在UI Thread中執行,兩者互不干擾。  如果子執行緒過多時,程式碼就會顯得很臃腫、可讀性不好。所以android 系統中集成了一個AsyncTask工具類用來包裝Thread+Handler,這個類的主要在子執行緒執行耗時操作,然後在UI執行緒更新UI。同時使得程式碼的可讀性好多了。關於AsyncTask有專門介紹。      5.2 ANR    在使用android系統的時候或多或少可能遇到過ANR(Application Not Responding), 類似的這種ANR對話方塊應該都見過。           為什麼會出現:android中的所有元件預設都是在UI執行緒中完成的,其中有ActivityManagerService(AMS)和WindowsManagerService(WMS)會監控一個動作的響應時間,如果在一定範圍內對使用者互動沒有處理完畢就會出現ANR。比如說分派事件,假如某一段時間內UI 執行緒做了耗時操作,執行緒阻塞了,那麼使用者的一個觸控事件就可能來不得及處理,超過一定的時間就會導致ANR。當然這只是ANR其中的一種出現原因。對於用於來說一旦開始互動事件沒有響應,就有可能不停的觸控。這更增加了ANR的機率,對於開發者來說開發過程中一定儘量避免ANR出現的可能。      ANR出現有原因的,要同時滿足幾個條件:      1.主執行緒也就是必須在UI執行緒中相應超時。所以我們可以將耗時操作放在子執行緒中執行來避免這個條件出現。      2.超時,必須超過一定的時間限制,不同的ANR的時間限制不一樣,下面有介紹。      3.互動時間或者特定操作。不同的ANR觸發的時間不一樣。      主要有三種ANR型別 1.KeyDispatchTimeOut(5s),主執行緒對使用者互動事件5s中沒有處理完畢(這種情況最常見):當app獲得使用者互動事件時(按鍵,觸控),系統先獲得這個互動事件然後分派到這個app,分派到app之後再分派給對應的View(比如button)。如果在5s內,該互動事件沒有處理完成就會通過一系列的回撥函式,最終到AMS中處理該處理keyDispatchTimeOut超時。        一定要注意:出現這種ANR前提是有使用者互動事件,如果沒有互動事件,就算是主執行緒阻塞主執行緒也不會ANR,因為這個時候就沒有事件派發。可以測試,如果僅僅是Thread.sleep(60000),不做任何互動都不會出現ANR(看具體的裝置和系統,我的榮耀3c暢玩版就不會出現,HTC的就出現)。      2.BroadcastTimeOut(10s)BroadcastReceiver 中的onReceiver方法執行超過10s。                3.ServiceTimeout(20s),service生命週期方法中執行事件超過20s。      2和3中不會產生對話方塊形式(和裝置有關。我的華為榮耀3c暢玩版、htc都出現了)。這三種anr的出現影響最大的就是第一個,但是三者之間並不是單一出現的,有可能receiver中執行時阻塞,使用者互動,系統沒法及時處理該互動,出現第一種;如果receiver超時後又出現第二種。不管怎樣,避免這三種操作就能最大程度的避免ANR的出現。      出現ANR時,有兩個輸出檔案要重視:一個就是系統輸出的log tag,一個就是/data/anr/traces.txt,這兩個檔案使我們定位ANR的利器      先看系統的log:   //ANR 在哪個程序發生了ANR       05-14 15:24:10.519: E/ActivityManager(499): ANR in com.example.servicedemo, time=1037238      //reson指明原因       05-14 15:24:10.519: E/ActivityManager(499): Reason: Broadcast of Intent { act=com.service.ACTION flg=0x10 cmp=com.example.servicedemo/.MyReceiver }      //ago 指明ANR之前cup使用情況           05-14 15:24:10.519: E/ActivityManager(499): CPU usage from 4973ms to -1016msago:      //later指明ANR之後cpu使用情況 05-14 15:24:10.519: E/ActivityManager(499): CPU usage from 472ms to 993mslater:      再來看traces.txt內容:(目錄:/data/anr/traces.txt)      ----- pid 5085 at 2015-05-14 15:24:09 -----
     Cmd line: com.example.servicedemo
     DALVIK THREADS:
     (mutexes: tll=0 tsl=0 tscl=0 ghl=0)
     "main" prio=5 tid=1 TIMED_WAIT
       | group="main" sCount=1 dsCount=0 obj=0x40ae3490 self=0x15cad58
       | sysTid=5085 nice=0 sched=0/0 cgrp=default handle=1074472136
       | schedstat=( 0 0 0 ) utm=17 stm=6 core=0
       at java.lang.VMThread.sleep(Native Method)
       at java.lang.Thread.sleep(Thread.java:1047)
       at java.lang.Thread.sleep(Thread.java:1029)
       at com.example.servicedemo.MyReceiver.onReceive(MyReceiver.java:19)
               這兩個資訊是如何輸出的:在ams中的appNotResponding函式:   synchronized (this) {                //在某些情況下忽略ANR
            // PowerManager.reboot() can block for a long time, so ignore ANRs while shutting down.
            if (mShuttingDown) {
                Slog.i(TAG, "During shutdown skipping ANR: " + app + " " + annotation);
                return;
            } else if (app.notResponding) {
                Slog.i(TAG, "Skipping duplicate ANR: " + app + " " + annotation);
                return;
            } else if (app.crashing) {
                Slog.i(TAG, "Crashing app skipping ANR: " + app + " " + annotation);
                return;
            }
         //.....           // Log the ANR to the main log.         StringBuilder info = mStringBuilder;
        info.setLength(0);
        info.append("ANR in ").append(app.processName);
        if (activity != null && activity.shortComponentName != null) {
            info.append(" (").append(activity.shortComponentName).append(")");
        }         info.append("\n");
        if (annotation != null) {
            info.append("Reason: ").append(annotation).append("\n");
        }
        if (parent != null && parent != activity) {
            info.append("Parent: ").append(parent.shortComponentName).append("\n");
        }      //指定traces檔案儲存地址      File tracesFile = dumpStackTraces(true, firstPids, processStats, lastPids);      String cpuInfo = null;
        if (MONITOR_CPU_USAGE) {
            updateCpuStatsNow();
            synchronized (mProcessStatsThread) {
                cpuInfo = mProcessStats.printCurrentState(anrTime);
            }                //anr之前cpu負載資訊
            info.append(processStats.printCurrentLoad());
            info.append(cpuInfo);
        }
        //anr之後cpu負載資訊 
        info.append(processStats.printCurrentState(anrTime));      //將anr資訊加入到dropBox中(管理log的工具)      addErrorToDropBox("anr", app, activity, parent, annotation, cpuInfo, tracesFile, null);      // Unless configured otherwise, swallow ANRs in background processes & kill the process.      //讀取使用者配置,是否顯示後臺程序anr對話方塊。不顯示就直接殺死         boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
                Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;         synchronized (this) {             if (!showBackground && !app.isInterestingToUserLocked() && app.pid != MY_PID) {
                Slog.w(TAG, "Killing " + app + ": background ANR");
                EventLog.writeEvent(EventLogTags.AM_KILL, app.pid,
                        app.processName, app.setAdj, "background ANR");
                Process.killProcessQuiet(app.pid);//殺死程序
                return;
            }
           // Bring up the infamous App Not Responding dialog,傳送資訊給mHandler,提示可以顯示ANR對話方塊
            Message msg = Message.obtain();
            HashMap map = new HashMap();
            msg.what = SHOW_NOT_RESPONDING_MSG;
            msg.obj = map;
            map.put("app", app);
            if (activity != null) {
                map.put("activity", activity);
            }
            mHandler.sendMessage(msg);           mHandler中handleMessage函式中相應的處理為:            case SHOW_NOT_RESPONDING_MSG: {
                synchronized (ActivityManagerService.this) {                                                                   Dialog d = new AppNotRespondingDialog(ActivityManagerService.this,
                            mContext, proc, (ActivityRecord)data.get("activity"));
                    d.show();//顯示anr對話方塊           
                }            
            } break;      如何避免:ANR產生的原因是因為在主執行緒中執行了耗時的操作,所以將耗時操作方法在子執行緒中執行,可以使用handler+thread 或者AsyncTask方式。      同時為了給使用者好的體驗,也有幾個建議:           1.執行耗時操作時,給使用者ui提示,比如進度對話方塊。      2.遊戲類app用子執行緒計算。      3.應用程式的初始化,可以使用一個splash或者過場動畫,這種使用的很多比如一開始開啟app時顯示一張歡迎圖片(天天動聽),這樣既給使用者app正在響應的感覺,同時可以執行耗時操作。