Android面试必会知识点 - ANR详解

    最近在公司出差过多,感觉自己快被废了,这时候正好有大公司给了面试机会,于是就去试试,虽然最后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很注重题目,不注重实战,想要进的朋友就背题吧~大公司不会轻易让你掌握核心,所以很养老,三四十的时候进去最好~现在还是多学点实战好~个人认为~

相关文章
相关标签/搜索