1. 程式人生 > >Android面試必會知識點 - ANR詳解

Android面試必會知識點 - ANR詳解

除了 信息 load 本地 leg 主線程 阻塞 tst 第一次

最近在公司出差過多,感覺自己快被廢了,這時候正好有大公司給了面試機會,於是就去試試,雖然最後Tencent沒有要我,但是過程中讓我對Android有了更新的認知,把我的對於Android的理解又提升了一步,而不是僅僅對於Android應用層的理解,在底層的實現有了更深的認知,希望我的這幾次面試能幫到你~

ANR問題:

1.什麽是ANR? ANR:Application Not Responding,即應用無響應。


2.ANR類型:

1:KeyDispatchTimeout(5 seconds) --主要類型 按鍵或觸摸事件在特定時間內無響應

2:BroadcastTimeout(10 seconds) --次要類型 BroadcastReceiver在特定時間內無法處理完成

3:ServiceTimeout(20 seconds) --小概率類型 Service在特定的時間內無法處理完成


KeyDispatchTimeout

Akey or touch event was not dispatched within the specified time(按鍵或觸摸事件在特定時間內無響應)

具體的超時時間的定義在framework下的

ActivityManagerService.java

//How long we wait until we timeout on key dispatching.

staticfinal int KEY_DISPATCHING_TIMEOUT = 5*1000


為什麽會超時呢?

超時時間的計數一般是從按鍵分發給app開始。超時的原因一般有兩種:

(1)當前的事件沒有機會得到處理(即UI線程正在處理前一個事件,沒有及時的完成或者looper被某種原因阻塞住了)

(2)當前的事件正在處理,但沒有及時完成


如何避免KeyDispatchTimeout

1:UI線程盡量只做跟UI相關的工作

2:耗時的工作(比如數據庫操作,I/O,連接網絡或者別的有可能阻礙UI線程的操作)把它放入單獨的線程處理

3:盡量用Handler來處理UIthread和別的thread之間的交互


說了那麽多的UI線程,那麽哪些屬於UI線程呢?

UI線程主要包括如下:

1. Activity:onCreate(), onResume(), onDestroy(), onKeyDown(), onClick(),etc

2. AsyncTask: onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel,etc

3. Mainthread handler: handleMessage(), post*(runnable r), etc

4. other


如何避免ANR

1、運行在主線程裏的任何方法都盡可能少做事情。特別是,Activity應該在它的關鍵生命周期方法(如onCreate()和onResume())裏盡可能少的去做創建操作。(可以采用重新開啟子線程的方式,然後使用Handler+Message的方式做一些操作,比如更新主線程中的ui等)


2、應用程序應該避免在BroadcastReceiver裏做耗時的操作或計算。但不再是在子線程裏做這些任務(因為 BroadcastReceiver的生命周期短),替代的是,如果響應Intent廣播需要執行一個耗時的動作的話,應用程序應該啟動一個 Service。(此處需要註意的是可以在廣播接受者中啟動Service,但是卻不可以在Service中啟動broadcasereciver,關於原因後續會有介紹,此處不是本文重點)


3、避免在Intent Receiver裏啟動一個Activity,因為它會創建一個新的畫面,並從當前用戶正在運行的程序上搶奪焦點。如果你的應用程序在響應Intent廣播時需要向用戶展示什麽,你應該使用Notification Manager來實現。


4、通常100到200毫秒就會讓人察覺程序反應慢,為了更加提升響應, 如果程序正在後臺處理用戶的輸入,建議使用讓用戶得知進度,比如使用ProgressBar控件,程序啟動時可以選擇加上歡迎界面,避免讓用戶察覺卡頓,使用Systrace和TraceView找出影響響應的問題。


如何創造一個anr寫法:

新建一個Demo,在TextView的onClick事件中使用Thread.sleep()方法

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.tv_hello).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

直接運行,點擊TextView,稍等片刻ANR就隨之而來了。

技術分享圖片

獲取ANR產生的trace文件

ANR產生時, 系統會生成一個traces.txt的文件放在/data/anr/下. 可以通過adb命令將其導出到本地:

  $adb pull data/anr/traces.txt

普通阻塞導致的ANR

獲取到的tracs.txt文件一般如下:

如下以GithubApp代碼為例, 強行sleep thread產生的一個ANR.

----- pid 2976 at 2018年7月2日10:49:57 -----
Cmd line: com.anly.githubapp  // 最新的ANR發生的進程(包名)

...

DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
  | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
  | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
  | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:985) // 主線程中sleep過長時間, 阻塞導致無響應.
  at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
  - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 產生ANR的那個函數調用
  - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
  at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
  at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起點
  at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
  at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
  at android.view.View.performClick(View.java:4780)
  at android.view.View$PerformClick.run(View.java:19866)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5254)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

拿到trace信息, 一切好說.

如上trace信息中的添加的中文註釋已基本說明了trace文件該怎麽分析:

1.文件最上的即為最新產生的ANR的trace信息.

2.前面兩行表明ANR發生的進程pid, 時間, 以及進程名字(包名).

3.尋找我們的代碼點, 然後往前推, 看方法調用棧, 追溯到問題產生的根源.


以上的ANR trace是屬於相對簡單, 還有可能你並沒有在主線程中做過於耗時的操作, 然而還是ANR了. 這就有可能是如下兩種情況了:


CPU滿負荷

這個時候你看到的trace信息可能會包含這樣的信息:

Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...

100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait

最後一句表明了:

1.當是CPU占用100%, 滿負荷了.

2.其中絕大數是被iowait即I/O操作占用了.

此時分析方法調用棧, 一般來說會發現是方法中有頻繁的文件讀寫或是數據庫讀寫操作放在主線程來做了.


內存原因

其實內存原因有可能會導致ANR, 例如如果由於內存泄露, App可使用內存所剩無幾, 我們點擊按鈕啟動一個大圖片作為背景的activity, 就可能會產生ANR, 這時trace信息可能是這樣的:

// 以下trace信息來自網絡, 用來做個示例
Cmdline: android.process.acore

DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)

...

MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732

可以看到free的內存已所剩無幾.當然這種情況可能更多的是會產生OOM的異常…


ANR的處理

針對三種不同的情況, 一般的處理情況如下

1.主線程阻塞的

開辟單獨的子線程來處理耗時阻塞事務.

2.CPU滿負荷, I/O阻塞的

I/O阻塞一般來說就是文件讀寫或數據庫操作執行在主線程了, 也可以通過開辟子線程的方式異步執行.

3.內存不夠用的

增大VM內存, 使用largeHeap屬性, 排查內存泄露(這個在內存優化那篇細說吧)等.


深入一點

沒有人願意在出問題之後去解決問題.高手和新手的區別是, 高手知道怎麽在一開始就避免問題的發生. 那麽針對ANR這個問題, 我們需要做哪些層次的工作來避免其發生呢?


哪些地方是執行在主線程的

1.Activity的所有生命周期回調都是執行在主線程的.

2.Service默認是執行在主線程的.

3.BroadcastReceiver的onReceive回調是執行在主線程的.

4.沒有使用子線程的looper的Handler的handleMessage, post(Runnable)是執行在主線程的.

5.AsyncTask的回調中除了doInBackground, 其他都是執行在主線程的.

6.View的post(Runnable)是執行在主線程的.


使用子線程的方式有哪些

上面我們幾乎一直在說, 避免ANR的方法就是在子線程中執行耗時阻塞操作. 那麽在Android中有哪些方式可以讓我們實現這一點呢.


啟Thread方式

這個其實也是Java實現多線程的方式. 有兩種實現方法, 繼承Thread 或 實現Runnable接口:

繼承Thread

class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}

PrimeThread p = new PrimeThread(143);
p.start();

實現Runnable接口

class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}

PrimeRun p = new PrimeRun(143);
new Thread(p).start();

使用AsyncTask

這個是Android特有的方式, AsyncTask顧名思義, 就是異步任務的意思.

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    // Do the long-running work in here
    // 執行在子線程
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }

    // This is called each time you call publishProgress()
    // 執行在主線程
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    // This is called when doInBackground() is finished
    // 執行在主線程
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

// 啟動方式
new DownloadFilesTask().execute(url1, url2, url3);


HandlerThread

Handler 必須要和 Looper 中結合使用,尤其在子線程中創建 Handler 的話,需要這樣寫:

class LooperThread extends Thread {
    public Handler mHandler;

      public void run() {
          Looper.prepare();

          mHandler = new Handler() {
              public void handleMessage(Message msg) {
                  // 這裏處理消息
              }
          };

          Looper.loop();
      }

可以看到,非常繁瑣,一層套一層看著也不美觀。HandlerThread 就是為了幫我們免去寫上面那樣的代碼而生的。

舉個栗子

我們寫一個使用 HandlerThread 實現子線程完成多個下載任務的 demo。

先創建一個 HandlerThread 子類,它有兩個 Handler 類型的成員變量,一個是用於在子線程傳遞、執行任務,另一個用於外部傳入,在主線程顯示下載狀態:

/**
 * Description:
 * <br> 繼承 HandlerThread 模擬下載線程
 * <p>
 */

public class DownloadThread extends HandlerThread implements Handler.Callback {

    private final String TAG = this.getClass().getSimpleName();
    private final String KEY_URL = "url";
    public static final int TYPE_START = 1;
    public static final int TYPE_FINISHED = 2;

    private Handler mWorkerHandler;
    private Handler mUIHandler;
    private List<String> mDownloadUrlList;

    public DownloadThread(final String name) {
        super(name);
    }

    @Override
    protected void onLooperPrepared() {    //執行初始化任務
        super.onLooperPrepared();
        mWorkerHandler = new Handler(getLooper(), this);    //使用子線程中的 Looper
        if (mUIHandler == null) {
            throw new IllegalArgumentException("Not set UIHandler!");
        }

        //將接收到的任務消息挨個添加到消息隊列中
        for (String url : mDownloadUrlList) {
            Message message = mWorkerHandler.obtainMessage();
            Bundle bundle = new Bundle();
            bundle.putString(KEY_URL, url);
            message.setData(bundle);
            mWorkerHandler.sendMessage(message);
        }
    }

    public void setDownloadUrls(String... urls) {
        mDownloadUrlList = Arrays.asList(urls);
    }

    public Handler getUIHandler() {
        return mUIHandler;
    }

    //註入主線程 Handler
    public DownloadThread setUIHandler(final Handler UIHandler) {
        mUIHandler = UIHandler;
        return this;
    }

    /**
     * 子線程中執行任務,完成後發送消息到主線程
     *
     * @param msg
     * @return
     */
    @Override
    public boolean handleMessage(final Message msg) {
        if (msg == null || msg.getData() == null) {
            return false;
        }

        String url = (String) msg.getData().get(KEY_URL);


        //下載開始,通知主線程
        Message startMsg = mUIHandler.obtainMessage(TYPE_START, "\n 開始下載 @" + DateUtils.getCurrentTime() + "\n" + url);
        mUIHandler.sendMessage(startMsg);

        SystemClock.sleep(2000);    //模擬下載

        //下載完成,通知主線程
        Message finishMsg = mUIHandler.obtainMessage(TYPE_FINISHED, "\n 下載完成 @" + DateUtils.getCurrentTime() + "\n" + url);
        mUIHandler.sendMessage(finishMsg);

        return true;
    }

    @Override
    public boolean quitSafely() {
        mUIHandler = null;
        return super.quitSafely();
    }
}

可以看到,DownloadThread 中做了以下工作:

1.創建一個子線程 Handler

2.然後在 onLooperPrepared()中初始化 Handler,使用的是 HandlerThread 創建的 Looper

同時將外部傳入的下載 url 以 Message 的方式發送到子線程中的 MessageQueue 中

3.這樣當調用 DownloadThread.start() 時,子線程中的 Looper 開始工作,會按順序取出消息隊列中的隊列處理,然後調用子線程的 Handler 處理

4.也就是上面的 handleMessage() 方法,在這個方法中進行耗時任務

5.然後通過 mUIHandler 將下載狀態信息傳遞到主線程

調用 Activity 的代碼:

/**
 * Description:
 * <br> HandlerThread 示例程序
 * <p>
 */


public class HandlerThreadActivity extends AppCompatActivity implements Handler.Callback {

    @BindView(R.id.tv_start_msg)
    TextView mTvStartMsg;
    @BindView(R.id.tv_finish_msg)
    TextView mTvFinishMsg;
    @BindView(R.id.btn_start_download)
    Button mBtnStartDownload;

    private Handler mUIHandler;
    private DownloadThread mDownloadThread;

    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_thread_test);
        ButterKnife.bind(this);
        init();
    }

    private void init() {
        mUIHandler = new Handler(this);
        mDownloadThread = new DownloadThread("下載線程");
        mDownloadThread.setUIHandler(mUIHandler);
        mDownloadThread.setDownloadUrls("http://pan.baidu.com/s/1qYc3EDQ", "http://bbs.005.tv/thread-589833-1-1.html", "http://list.youku.com/show/id_zc51e1d547a5b11e2a19e.html?");

    }

    @OnClick(R.id.btn_start_download)
    public void startDownload() {
        mDownloadThread.start();
        mBtnStartDownload.setText("正在下載");
        mBtnStartDownload.setEnabled(false);
    }

    @Override
    public boolean handleMessage(final Message msg) {
        switch (msg.what) {
            case DownloadThread.TYPE_FINISHED:
                mTvFinishMsg.setText(mTvFinishMsg.getText().toString() + "\n " + msg.obj);
                break;
            case DownloadThread.TYPE_START:
                mTvStartMsg.setText(mTvStartMsg.getText().toString() + "\n " + msg.obj);
                break;
        }
        return true;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mDownloadThread.quitSafely();
    }
}

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center_horizontal"
              android:orientation="vertical"
              android:padding="8dp">

    <TextView
        android:id="@+id/tv_start_msg"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:text="下載開始信息"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/colorAccent"/>

    <TextView
        android:id="@+id/tv_finish_msg"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginTop="8dp"
        android:text="下載完成信息"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/colorAccent"/>

    <Button
        android:id="@+id/btn_start_download"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="開始下載"/>
</LinearLayout>

重點是 init() 方法:

 private void init() {
        mUIHandler = new Handler(this);
        mDownloadThread = new DownloadThread("下載線程");
        mDownloadThread.setUIHandler(mUIHandler);
        mDownloadThread.setDownloadUrls("http://pan.baidu.com/s/1qYc3EDQ",
                            "http://bbs.005.tv/thread-589833-1-1.html", "http://list.youku.com/show/id_zc51e1d547a5b11e2a19e.html?");
    }

在這個方法中我們創建一個 DownloadThread,也就是 HandlerThread,然後傳入 UI 線程中的 Handler。最後在按鈕的點擊事件中調用了 start() 方法。

運行結果:

技術分享圖片


總結:

技術分享圖片

上面的例子中 HandlerThread 配合一個主線程 Handler 完成了在子線程中串行執行任務,同時在主線程中反饋狀態的功能。

如果用一句話總結 HandlerThread 的特點:它就是一個幫我們創建 Looper 的線程,讓我們可以直接在線程中使用 Handler 來處理異步任務。


Android中結合Handler和Thread的一種方式. 前面有雲, 默認情況下Handler的handleMessage是執行在主線程的, 但是如果我給這個Handler傳入了子線程的looper, handleMessage就會執行在這個子線程中的. HandlerThread正是這樣的一個結合體:

// 啟動一個名為new_thread的子線程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();

// 取new_thread賦值給ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
      super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
      // 此時handleMessage是運行在new_thread這個子線程中了.
    }
}

HandlerThread的特點


HandlerThread將loop轉到子線程中處理,說白了就是將分擔MainLooper的工作量,降低了主線程的壓力,使主界面更流暢。


開啟一個線程起到多個線程的作用。處理任務是串行執行,按消息發送順序進行處理。HandlerThread本質是一個線程,在線程內部,代碼是串行處理的。


但是由於每一個任務都將以隊列的方式逐個被執行到,一旦隊列中有某個任務執行時間過長,那麽就會導致後續的任務都會被延遲處理。


HandlerThread擁有自己的消息隊列,它不會幹擾或阻塞UI線程。


對於網絡IO操作,HandlerThread並不適合,因為它只有一個線程,還得排隊一個一個等著。




IntentService

Service是運行在主線程的, 然而IntentService是運行在子線程的.實際上IntentService就是實現了一個HandlerThread + ServiceHandler的模式.

以上HandlerThread的使用代碼示例也就來自於IntentService源碼.


TIPS:

使用Thread和HandlerThread時, 為了使效果更好, 建議設置Thread的優先級偏低一點:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);(慎重使用)


因為如果沒有做任何優先級設置的話, 你創建的Thread默認和UI Thread是具有同樣的優先級的, 你懂的. 同樣的優先級的Thread, CPU調度上還是可能會阻塞掉你的UI Thread, 導致ANR的.


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

技術分享圖片


大多數情況下,新創建的線程優先級會被設置為默認的0,主線程設置為0的時候,新創建的線程還可以利用 THREAD_PRIORITY_LESS_FAVORABLE 或者 THREAD_PRIORITY_MORE_FAVORABLE 來控制線程的優先級。

技術分享圖片

技術分享圖片


IntentService 簡介

public abstract class IntentService extends Service {...}

IntentService 是一個抽象類,繼承了 Service

由於是一個 Service,IntentService 的優先級比較高,在後臺不會輕易被系統殺死;它可以接收 Intent 請求,然後在子線程中按順序執行。

官方文檔關於它的介紹:

IntentService 使用工作線程逐一處理所有啟動請求。如果你不需要在 Service 中執行並發任務,IntentService 是最好的選擇。


IntentService 源碼很短:

public abstract class IntentService extends Service {
    private volatile Looper mServiceLooper;
    private volatile ServiceHandler mServiceHandler;
    private String mName;
    private boolean mRedelivery;

    //內部創建的 Handler
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            //調用這個方法處理數據
            onHandleIntent((Intent)msg.obj);
            //處理完就自盡了
            stopSelf(msg.arg1);
        }
    }

    //子類需要重寫的構造函數,參數是服務的名稱
    public IntentService(String name) {
        super();
        mName = name;
    }

    //設置當前服務被意外關閉後是否重新
    //如果設置為 true,onStartCommand() 方法將返回 Service.START_REDELIVER_INTENT,這樣當
    //當前進程在 onHandleIntent() 方法返回前銷毀時,會重啟進程,重新使用之前的 Intent 啟動這個服務
    //(如果有多個 Intent,只會使用最後的一個)
    //如果設置為 false,onStartCommand() 方法返回 Service.START_NOT_STICKY,當進程銷毀後也不重啟服務
    public void setIntentRedelivery(boolean enabled) {
        mRedelivery = enabled;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //創建時啟動一個 HandlerThread
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();

        //拿到 HandlerThread 中的 Looper,然後創建一個子線程中的 Handler
        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        //將 intent 和 startId 以消息的形式發送到 Handler
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

    /**
     * You should not override this method for your IntentService. Instead,
     * override {@link #onHandleIntent}, which the system calls when the IntentService
     * receives a start request.
     * @see android.app.Service#onStartCommand
     */
    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        onStart(intent, startId);
        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        mServiceLooper.quit();    //值得學習的,在銷毀時退出 Looper
    }

    @Override
    @Nullable
    public IBinder onBind(Intent intent) {
        return null;
    }

    @WorkerThread
    protected abstract void onHandleIntent(@Nullable Intent intent);
}

從上述代碼可以看到,IntentService 做了以下工作:


1.創建了一個 HandlerThread 默認的工作線程

2.使用 HandlerThread 的 Looper 創建了一個 Handler,這個 Handler 執行在子線程

3.在onStartCommand() 中調用 onStart(),然後在 onStart() 中將 intent 和 startId 以消息的形式發送到 Handler

4.在 Handler 中將消息隊列中的 Intent 按順序傳遞給 onHandleIntent() 方法

5.在處理完所有啟動請求後自動停止服務,不需要我們調用 stopSelf()

public void handleMessage(Message msg) {
    onHandleIntent((Intent)msg.obj);
    stopSelf(msg.arg1);
}

有同學可能有疑問,在 handleMessage 方法中不是調用了一次 onHandleIntent() 後就調用 stopSelf() 了嗎,這不是只能執行一個任務麽?

仔細看下可以發現,這個 stopSelf() 方法傳遞了一個 id,這個 id 是啟動服務時 IActivityManager 分配的 id,當我們調用 stopSelf(id) 方法結束服務時,IActivityManager 會對比當前 id 是否為最新啟動該服務的 id,如果是就關閉服務。

public final void stopSelf(int startId) {
    if (mActivityManager == null) {
        return;
    }
    try {
        mActivityManager.stopServiceToken(
                new ComponentName(this, mClassName), mToken, startId);
    } catch (RemoteException ex) {
    }
}

因此只有當最後一次啟動 IntentService 的任務執行完畢才會關閉這個服務。

此外還要註意的是,IntentService 中除了 onHandleIntent 方法其他都是運行在主線程的。


IntentService 的使用

通過前面的源碼分析,我們可以看到,最終每個任務的處理都會調用 onHandleIntent(),因此使用 IntentService 也很簡單,只需實現 onHandleIntent() 方法,在這裏執行對應的後臺工作即可。

舉個例子:

我們寫一個使用 IntentService 實現在子線程下載多張 美女圖片 的效果。

創建 IntentService 的子類

/**
 * Description:
 * <br> 使用 IntentService 實現下載
 * <p>
 */

public class DownloadService extends IntentService {
    private static final String TAG = "DownloadService";
    public static final String DOWNLOAD_URL = "down_load_url";
    public static final int WHAT_DOWNLOAD_FINISHED = 1;
    public static final int WHAT_DOWNLOAD_STARTED = 2;

    public DownloadService() {
        super(TAG);
    }

    private static Handler mUIHandler;

    public static void setUIHandler(final Handler UIHandler) {
        mUIHandler = UIHandler;
    }

    /**
     * 這個方法運行在子線程
     *
     * @param intent
     */
    @Override
    protected void onHandleIntent(final Intent intent) {
        String url = intent.getStringExtra(DOWNLOAD_URL);
        if (!TextUtils.isEmpty(url)) {
            sendMessageToMainThread(WHAT_DOWNLOAD_STARTED, "\n " + DateUtils.getCurrentTime() + " 開始下載任務:\n" + url);
            try {
                Bitmap bitmap = downloadUrlToBitmap(url);
                SystemClock.sleep(1000);    //延遲一秒發送消息
                sendMessageToMainThread(WHAT_DOWNLOAD_FINISHED, bitmap);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 發送消息到主線程
     *
     * @param id
     * @param o
     */
    private void sendMessageToMainThread(final int id, final Object o) {
        if (mUIHandler != null) {
            mUIHandler.sendMessage(mUIHandler.obtainMessage(id, o));
        }
    }

    /**
     * 下載圖片
     *
     * @param url
     * @return
     * @throws Exception
     */
    private Bitmap downloadUrlToBitmap(String url) throws Exception {
        HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
        BufferedInputStream in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
        Bitmap bitmap = BitmapFactory.decodeStream(in);
        urlConnection.disconnect();
        in.close();
        return bitmap;
    }
}


在上面的代碼中,我們做了以下幾件事:

  • onHandleIntent() 中接收任務,開始下載,同時將狀態返回給主線程

  • 下載完成後將得到的 Bitmap 通過 Handler 發送到主線程

為了界面上有明顯效果,設置了一定延時。IntentService 也是 Service,別忘了在 AndroidManifest 中註冊!

<service    
android:name=".DownloadService "    
android:enabled="true"    
android:exported="true"    
android:process=":downloadservice "/>

布局界面

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center_horizontal"
              android:orientation="vertical"
              android:padding="8dp">

    <ImageView
        android:id="@+id/iv_display"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <TextView
        android:id="@+id/tv_status"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:padding="8dp"
        android:text="狀態信息:"/>

    <Button
        android:id="@+id/btn_download"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="開始下載"/>
</LinearLayout>

界面上有一個開始下載按鈕,一個顯示下載狀態的 TextView,一個展示圖片的 ImageView.

調用方代碼

/**
 * Description:
 * <br> IntentService 實例
 * <p>
 */

public class IntentServiceActivity extends AppCompatActivity implements Handler.Callback {

    @BindView(R.id.iv_display)
    ImageView mIvDisplay;
    @BindView(R.id.btn_download)
    Button mBtnDownload;
    @BindView(R.id.tv_status)
    TextView mTvStatus;

    private List<String> urlList = Arrays.asList("https://ws1.sinaimg.cn/large/610dc034ly1fgepc1lpvfj20u011i0wv.jpg",
            "https://ws1.sinaimg.cn/large/d23c7564ly1fg6qckyqxkj20u00zmaf1.jpg",
            "https://ws1.sinaimg.cn/large/610dc034ly1fgchgnfn7dj20u00uvgnj.jpg");
    int mFinishCount;   //完成的任務個數

    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_intent_service);
        ButterKnife.bind(this);
        DownloadService.setUIHandler(new Handler(this));
    }

    @OnClick(R.id.btn_download)
    public void downloadImage() {
        Intent intent = new Intent(this, DownloadService.class);

        for (String url : urlList) {
            intent.putExtra(DownloadService.DOWNLOAD_URL, url);
            startService(intent);
        }
        mBtnDownload.setEnabled(false);
    }


    @Override
    public boolean handleMessage(final Message msg) {
        if (msg != null) {
            switch (msg.what) {
                case DownloadService.WHAT_DOWNLOAD_FINISHED:
                    mIvDisplay.setImageBitmap((Bitmap) msg.obj);
                    mBtnDownload.setText("完成 " + (++mFinishCount) + "個任務");
                    break;
                case DownloadService.WHAT_DOWNLOAD_STARTED:
                    mTvStatus.setText(mTvStatus.getText() + (String) msg.obj);
                    break;
            }
        }
        return true;
    }
}

Activity 中做了以下幾件事:

  • 設置 UI 線程的 Handler 給 IntentService

  • 使用 startService(intent) 啟動 IntentService 執行圖片下載任務

  • 在 Handler 的 handleMessage 中根據消息類型進行相應處理

可以看到,調用方的代碼和上一篇使用 HandlerThread 的方法很相似。

運行效果

技術分享圖片


總結

介紹了 IntentService 的使用和源碼。


在第一次啟動 IntentService 後,IntentService 仍然可以接受新的請求,接受到的新的請求被放入了工作隊列中,等待被串行執行。


使用 IntentService 顯著簡化了啟動服務的實現,如果您決定還重寫其他回調方法(如 onCreate()、onStartCommand() 或 onDestroy()),請確保調用超類實現,以便 IntentService 能夠妥善處理工作線程的生命周期。由於大多數啟動服務都不必同時處理多個請求(實際上,這種多線程情況可能很危險),因此使用 IntentService 類實現服務也許是最好的選擇。


一句話總結 IntentService:

優先級比較高的、用於串行執行異步任務、會自盡的 Service。


BAT很註重題目,不註重實戰,想要進的朋友就背題吧~大公司不會輕易讓你掌握核心,所以很養老,三四十的時候進去最好~現在還是多學點實戰好~個人認為~

技術分享圖片



Android面試必會知識點 - ANR詳解