深入浅出handler(一)

181 阅读7分钟

我们都知道Android中只有主线程可以操作UI

有些爱刨根问底的同学可能就要问了:

为什么只有主线程可以更改UI呢?

回答这个问题前,我们都知道多线程操作资源时是存在并发这个问题的。而为了解决并发,我们会使用乐观锁、悲观锁、自旋锁等多种操作来避免多线程抢占资源时可能出现的死锁等问题。

Android本身就已经够复杂了,那为了让操作简单点,于是Android规定,只有主线程可以更新UI,这样就避免了添加锁,也让UI的绘制变得更加高效。

于此同时又带来一个新的问题,那子线程做了一些耗时操作就是想更新一下UI呢?

2022-09-16 13:03:31.258 5957-5984/com.example.testapt E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.example.testapt, PID: 5957
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8031)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1276)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:360)
        at android.view.View.requestLayout(View.java:23120)
        at android.widget.TextView.checkForRelayout(TextView.java:8933)
        at android.widget.TextView.setText(TextView.java:5755)
        at android.widget.TextView.setText(TextView.java:5596)
        at android.widget.TextView.setText(TextView.java:5553)
        at com.example.testapt.FirstMainActivity$1.run(FirstMainActivity.java:45)

上图是在子线程中更新UI出现的报错,当然有些小伙伴会说我也在Thread中sleep了,为啥没有丝毫报错呢?(为了不打断本文的节奏,具体的原因后续更~)

现实中,做了耗时任务的子线程很多,比如网络加载,比如一些异步任务,当做完这些异步任务,该如何通知UI更新呢?

于是,Android推出了Handler机制,这也正是本文的重点。

一、主线程中定义Handler

handler的一个简单栗子如下:

public class FirstMainActivity extends AppCompatActivity {

   private ActivityMainBinding binding;

   public static Handler handler = new Handler() {
       @Override
       public void handleMessage(@NonNull Message msg) {
           super.handleMessage(msg);
           if (msg.what == 100) {
               Log.i("FirstMainActivity", "接收到来自子线程的handler消息..." + msg.arg1);
           }
       }
   };

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivityMainBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       binding.fab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View view) {

               new Thread() {
                   @Override
                   public void run() {
                       super.run();
                       Message message = new Message();
                       message.what = 100;
                       message.arg1 = 2008;
                       handler.sendMessage(message);

                   }
               }.start();

           }
       });
   }
   
}

我们可以看到这个handler是实例化在Activity中的,子线程处理好任务后,使用这个Handler抛出消息,并且还可以绑定一些数据发送,于此同时,在Activity中实例化了的handler在handleMessage中处理线程发送的数据,进行相应的数据处理。

通过sendMessage和handleMessage就丝滑地实现了现场的切换,是不是超级方便?

二、在子线程中定义Handler

又有爱折腾的Android要问了,如果我在子线程中实例化一个Handler呢?

我们尝试着在子线程中去handleMessage,主线程去sendMessage

public class FirstMainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private Handler handler;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        binding.fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {


                Message message = new Message();
                message.what = 100;
                message.arg1 = 2008;
                handler.sendMessage(message);


            }
        });
        new Thread() {
            @Override
            public void run() {
                super.run();
                handler = new Handler() {
                    @Override
                    public void handleMessage(@NonNull Message msg) {
                        super.handleMessage(msg);
                        if (msg.what == 100) {
                            Log.i("FirstMainActivity", "接收到来自主线程的handler消息..." + msg.arg1);
                        }
                    }
                };
            }
        }.start();

    }

}

看上去只是将handler实例化的位置做了一个调换,这下我们却彻底run不起来了。

2022-09-16 13:18:27.237 7042-7076/com.example.testapt E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.example.testapt, PID: 7042
    java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
        at android.os.Handler.<init>(Handler.java:205)
        at android.os.Handler.<init>(Handler.java:118)
        at com.example.testapt.FirstMainActivity$2$1.<init>(FirstMainActivity.java:59)
        at com.example.testapt.FirstMainActivity$2.run(FirstMainActivity.java:59)

程序直接报错如上:

 Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()

在初始化Handler对象之前,需要调用Looper.prepare()方法。

可能你又会疑惑了,那为啥主线程中就没这一大堆报错呢?

其实从报错中我们就可以看到,Handler的工作是依赖于Looper的。

Looper部分源码

//获取当前线程的Looper()
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

//准备looper
public static void prepare() {
    prepare(true);
}


private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

可以看到Looper是通过ThreadLocal实现线程隔离,从而达到线程安全的。

插入:

ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程无法获取。因此Handler通过Looper再间接与线程绑定在一起。

我们再找找哪里在主线程中设置了Looper的

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}


/**
 * Returns the application's main looper, which lives in the main thread of the application.
 */
public static Looper getMainLooper() {
    synchronized (Looper.class) {
        return sMainLooper;
    }
}

而在ActivityThread的main()函数入口

image.png

已经默认去给我们准备了主线程的Looper了。

同样我们需要在子线程中去初始化一下Looper

Looper.prepare();

然而此时我们发现子线程依然没有接收到任何消息。

在子线程和主线程中除了初始化Looper还做了什么预操作呢?

我们再看到Activity的main()函数


    Looper.prepareMainLooper();

    //省略部分代码
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

在调用了prepareMainLooper()之后,Looper还调用了loop()函数

@SuppressWarnings("AndroidFrameworkBinderIdentity")
public static void loop() {
    final Looper me = myLooper();
 
    
    //省略部分代码

    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block

可以看到利用for循环,轮询messageQueu里面的消息队列,因为开启了死循环,所以这个任务也是无法终止的,而后面的程序语句更是无法达到。

所以在main函数looper.loop()后又打印并抛出了一句异常

image.png

当我们回到子线程初始化handler之后,一起准备就绪后,也同样开启我们的looper.loop()轮询机制。

image.png

此时我们就能在子线程中接收到来自主线程分发的消息了。

然而这种Looper.loop()会让子线程一直处于等待的状态,主线程由于是UI线程,这种长期轮询等待消息的分发肯定是要一直开启。

而对于只处理一次耗时任务的线程来说,一直采用looper()循环无疑是消耗性能的。

所以注意,当我们在子线程中创建Looper后,在所有的任务执行完成后应该调用quit方法来终止消息循环。

image.png

三、handler的使用注意问题

既然handler如此好用,线程间传递消息如此顺滑,那么有没有什么要注意的事项呢?

message属于当前的handler,handler又属于当前的Looper,一个县城只有一个Looper和MessageQueue,而我们的主线程通常都是以Activity的形式展示在前台供用户交互。

如果我们的Message迟迟无法处理,或者有一个关于handler指向Activity的引用链无法释放,最终将导致已经被关闭的Activity无法被正常释放,从而导致内存泄露。

1、延时消息

在Activity销毁时移除所有Messages

public class FirstMainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private Handler handler = new Handler();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        //如果页面没到超时时间却关闭,此时引用链仍然存在,出现内存泄露
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                binding.tvShow.setText("测试延时消息");

            }
        }, 5000);
    }

}

2、内部类隐式调用

上述代码通过内部类的方式创建了handler对象,而内部类会隐式地持有外部类对象的引用,也就是FirstMainActivity,当执行postDelayed()方法时,该方法会将Handler装入一个Message,并且把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息。

那么当这个Activity退出时消息队列中还有未处理的消息,或者正在处理消息,而消息队列中的Message持有handler实例的引用,handler又持有Activity的引用,所以导致该Activity的内存资源无法被及时回收,引发内存泄露。

四、解决方案

1、移除所有消息

要想避免Handler引发内存泄露问题,需要我们在Activity关闭退出时移除消息队列中所有消息和所有的Runnable,主动断掉引用链,也就不会影响Activity的回收了。

在onDestroy()函数中调用

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);
}

2、用弱引用+静态内部类

我们可以在初始化Handler时用静态内部类+弱引用,这样在gc时,activity就可以被回收了

private static class MyHandler extends Handler {
    private WeakReference<FirstMainActivity> weakReference;

    //弱引用的方式,在gc时,activity可被回收
    public MyHandler(WeakReference<FirstMainActivity> weakReference) {
        this.weakReference = weakReference;
    }

    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
}