Android 异步任务和消息机制面试题分析

来自:网络
时间:2023-07-25
阅读:
目录

1.1 HandlerThread 的使用场景和用法?

HandlerThread 本质上是一个在子线程的handler (HandlerThread=Handler+Thread);

它的使用:

步骤1:创建HandlerThread实例对象

HandlerThread mHandlerThread = new HandlerThread("handlerThread");

步骤2:启动线程

mHandlerThread.start();

步骤3:创建工作线程Handler & 复写handleMessage()

Handler workHandler = new Handler(
handlerThread.getLooper() ) {
    @OverRide
    public boolean handleMessage(Message msg) {
        ...//消息处理
        return true;
    }
});

步骤4:使用工作线程Handler向工作线程的消息队列发送消息

Message msg = Message.obtain();
msg.what = 2; //消息的标识
msg.obj = "B"; // 消息的存放
// b. 通过Handler发送消息到其绑定的消息队列
workHandler.sendMessage(msg);

步骤5:结束线程,即停止线程的消息循环

mHandlerThread.quit();

优势:

  • 将loop运行在子线程中处理,减轻了主线程的压力,使主线程更流畅
  • 串行执行,开启一个线程起到多个线程的作用
  • 有自己的消息队列,不会干扰UI线程

劣势:

  • 由于每一个任务队列逐步执行,一旦队列耗时过长,消息延时
  • 对于IO等操作,线程等待,不能并发

1.2 IntentService 的应用场景和使用姿势?

IntentService 是 Service 的子类,默认为我们开启了一个工作线程,使用这个工作线程逐一处理所有启动请求,在任务执行完毕后会自动停止服务,使用简单,只要实现一个方法 onHandleIntent,该方法会接收每个启动请求的 Intent,能够执行后台工作和耗时操作。可以启动IntentService 多次,而每一个耗时操作会以队列的方式在 IntentService 的 onHandlerIntent 回调方法中执行,并且,每一次只会执行一个工作线程,执行完第一个再执行第二个。并且等待所有消息都执行完后才终止服务。
IntentService 适用于 APP 在不影响当前用户的操作的前提下,在后台默默的做一些操作。

IntentService源码:

  • 通过 HandlerThread 单独开启一个名为IntentService 的线程
  • 创建一个名叫 ServiceHandler 的内部 Handler
  • 把内部Handler与HandlerThread所对应的子线程进行绑定
  • 通过 onStartCommand() 传递给服务 intent,依次插入到工作队列中,并逐个发送给 onHandleIntent()
  • 通过 onHandleIntent() 来依次处理所有 Intent 请求对象所对应的任务

使用示例:

public class MyIntentService extends IntentService {
    public static final String TAG = "MyIntentService";
    public MyIntentService() {
        super("MyIntentService");
    }
    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        boolean isMainThread = Thread.currentThread() == Looper.getMainLooper().getThread();
        Log.i(TAG, "is main thread:" + isMainThread); // 这里会打印false,说明不是主线程
        // 模拟耗时操作
        download();
    }
    /**
     * 模拟执行下载
     */
    private void download() {
        try {
            Thread.sleep(5000);
            Log.i(TAG, "下载完成...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.3 AsyncTask的优点和缺点?

AsyncTask的实现原理:

  • AsyncTask是一个抽象类,主要由Handler+2个线程池构成,SERIAL_EXECUTOR是任务队列线程池,用于调度任务,按顺序排列执行,THREAD_POOL_EXECUTOR是执行线程池,真正执行具体的线程任务。Handler用于工作线程和主线程的异步通信。
  • AsyncTask<Params,Progress,Result>,其中Params是doInBackground()方法的参数类型,Result是doInBackground()方法的返回值类型,Progress是onProgressUpdate()方法的参数类型。
  • 当执行execute()方法的时候,其实就是调用SERIAL_EXECUTOR的execute()方法,就是把任务添加到队列的尾部,然后从头开始取出队列中的任务,调用THREAD_POOL_EXECUTOR的execute()方法依次执行,当队列中没有任务时就停止。
  • AsyncTask只能执行一次execute(params)方法,否则会报错。但是SERIAL_EXECUTOR和
    THREAD_POOL_EXECUTOR线程池都是静态的,所以可以形成队列。

Q:AsyncTask只能执行一次execute()方法,那么为什么用线程池队列管理 ?

因为SERIAL_EXECUTOR和THREAD_POOL_EXECUTOR线程池都是静态的,所有的AsyncTask实例都共享这2个线程池,因此形成了队列。

Q:AsyncTask的onPreExecute()、doInBackground()、onPostExecute()方法的调用流程?

AsyncTask在创建对象的时候,会在构造函数中创建mWorker(workerRunnable) mFuture(FutureTask)对象。
mWorker实现了Callable接口的call()方法,在call()方法中,调用了doInBackground()方法,并在最后调用了postResult()方法,也就是通过Handler发送消息给主线程,在主线程中调用AsyncTask的finish()方法,决定是调用onCancelled()还是onPostExecute().

mFuture实现了Runnable和Future接口,在创建对象时,初始化成员变量mWorker,在run()方法中,调用mWorker的call()方法。

当asyncTask执行execute()方法的时候,会先调用onPreExecute()方法,然后调用SERIAL_EXECUTOR的execute(mFuture),把任务加入到队列的尾部等待执行。执行的时候调用THREAD_POOL_EXECUTOR的execute(mFuture).

1.4 谈谈你对 Activity.runOnUiThread 的理解?

一般是用来将一个Runnable绑定到主线程,在runOnUiThread源码里面会判断当前Runnable是否是主线程,如果是直接run,如果不是,通过一个默认的空构造函数Handler将Runnable post 到looper里面,创建构造函数Handler,会默认绑定一个主线程的looper对象

1.5 子线程能否更新UI?为什么?

子线程是不能直接更新UI的注意这句话,是不能直接更新,不是不能更新(极端情况
下可更新)

绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制UI,极端情况就是,在Activity的onResume(含)之前的生命周期中子线程都可以进行更新UI,也就是 onCreate,onStart和onResume,此时主线程的绘制还没开始。

1.6 谈谈 Handler 机制和原理?

首先在UI线程我们创建了一个Handler实例对象,无论是匿名内部类还是自定义类生成的Handler实例对象,我们都需要对handleMessage方法进行重写,在handleMessage方法中我们可以通过参数msg来写接受消息过后UI线程的逻辑处理,接着我们创建子线程,在子线程中需要更新UI的时候,新建一个Message对象,并且将消息的数据记录在这个消息对象Message的内部,比如arg1,arg2,obj等,然后通过前面的Handler实例对象调用sendMessge方法把这个Message实例对象发送出去,之后这个消息会被存放于MessageQueue中等待被处理,此时MessageQueue的管家Looper正在不停的把MessageQueue存在的消息取出来,通过回调dispatchMessage方法将消息传递给Handler的handleMessage方法,最终前面提到的消息会被Looper从MessageQueue中取出来传递给handleMessage方法。

1.7 为什么在子线程中创建Handler会抛异常?

不能在还没有调用 Looper.prepare() 方法的线程中创建Handler。

因为抛出异常的地方,在mLooper 对象为null的时候,会抛出异常。说明这里的Looper.myLooper();的返回值是null。 只有调用了Looper.prepare()方法,才会构造一个Looper对象并在 ThreadLocal 存储当前线程的Looper 对象。

这样在调用 Looper.myLooper() 时,获取的结果就不会为null。

1.8 试从源码角度分析Handler的post和sendMessage方法的区别和应用场景?

handler.post和handler.sendMessage方法最后都会调用sendMessageAtTime方法进行消息的发送,但是在post方法中message是通过getPostMessage(Runnable r)这个方法获取的message,在这个方法中有这样一句代码m.callback = r ,给message的callback赋值为runnable对象,而在dispatchMessage这个方法中对消息进行分发的时候,先进行了msg.callback != null的判断,如果不为null,消息是通过handleCallback(msg);这个方法处理的,在这个方法中message.callback.run();调用的是post方法传递过来的runnable内的run方法处理消息,如果为空,再进行handler内部的callback判断mCallback != null,如果handler内的callback不为空,执行mCallback.handleMessage(msg)这个处理消息并判断返回是否为true,如果返回true,消息处理结束,如果返回false,消息交给handler的handleMessage(msg)处理。

所以区别就是调用post方法的消息是在post传递的Runnable对象的run方法中处理,而调用sendMessage方法需要重写handleMessage方法或者给handler设置callback,在callback的handleMessage中处理并返回true。

1.9 Handler中有Loop死循环,为什么没有阻塞主线程,原理是什么?

主线程挂起

Looper 是一个死循环, 不断的读取MessageQueue中的消息, loop 方法会调用 MessageQueue 的 next 方法来获取新的消息,next 操作是一个阻塞操作,当没有消息的时候 next 方法会一直阻塞, 进而导致 loop 一直阻塞,理论上 messageQueue.nativePollOnce 会让线程挂起-阻塞-block 住, 但是为什么, 在发送 delay 10s 的消息, 假设消息队列中, 目前只有这一个消息;

那么为什么在这 10s 内, UI是可操作的, 或者列表页是可滑动的, 或者动画还是可以执行的?

先不讲 nativePollOnce 是怎么实现的阻塞, 我们还知道, 另外一个 nativeWake, 是实现线程唤醒的;

那么什么时候会, 触发这个方法的调用呢, 就是在有新消息添加进来的时候, 可是并没有手动添加消息啊?

display 每隔16毫秒, 刷新一次屏幕;

SurfaceFlingerVsyncChoreographer 每隔16毫秒, 发送一个 vSync 信号;

FrameDisplayEventReceiver 收到信号后, 调用 onVsync方法, 通过 handler 消息发送到主线程处理, 所以就会有消息添加进来, UI线程就会被唤醒;

事实上, 安卓系统, 不止有一个屏幕刷新的信号, 还有其他的机制, 比如输入法和系统广播, 也会往主线程的MessageQueue 添加消息;

所以, 可以理解为, 主线程也是随时挂起, 随时被阻塞的;

系统怎么实现的阻塞与唤醒

这种机制是通过pipe(管道)机制实现的;

简单来说, 管道就是一个文件在管道的两端, 分别是两个打开文件的, 文件描述符, 这两个打开文件描述符, 都是对应同一个文件, 其中一个是用来读的, 别一个是用来写的;

一般的使用方式就是, 一个线程通过读文件描述符, 来读管道的内容, 当管道没有内容时, 这个线程就会进入等待状态,
而另外一个线程, 通过写文件描述符, 来向管道中写入内容,写入内容的时候, 如果另一端正有线程, 正在等待管道中的内容, 那么这个线程就会被唤醒;

这个等待和唤醒的操作是如何进行的呢, 这就要借助 Linux系统中的 epoll 机制了, Linux 系统中的 epoll 机制为处理 大批量句柄而作了改进的 poll,是 Linux 下多路复用 IO 接口 select/poll 的增强版本, 它能显著减少程序, 在大量并发连接中, 只有少量活跃的情况下的系统 CPU 利用率;

即当管道中有内容可读时, 就唤醒当前正在等待管道中的内容的线程;

怎么证明, 线程被挂起了

    @Override
    public void onCreateData(@Nullable Bundle
                                     bundle) {
        new Thread() {
            @SuppressLint("HandlerLeak")
            @Override
            public void run() {
                super.run();
                LogTrack.v("thread.id = " +
                        Thread.currentThread().getId());
                Looper.prepare();
                Handler handler = new
                        Handler(Looper.getMainLooper()) {
                            @Override
                            public void
                            handleMessage(Message msg) {
                                super.handleMessage(msg);
                                LogTrack.v("thread.id = "
                                                + Thread.currentThread().getId() + ", what =
                                        " + msg.what);
                            }
                        };
                LogTrack.w("loop.之前"); // 执行了
                Looper.loop(); // 执行了
                LogTrack.w("loop.之后"); // 无法执行
            }
        }.start();
    }

以上就是Android 异步任务和消息机制面试题分析的详细内容,更多关于Android 异步任务消息机制的资料请关注其它相关文章!

返回顶部
顶部