Android Handler用法

发布于:2024-05-02 ⋅ 阅读:(31) ⋅ 点赞:(0)

参考文档:https://developer.android.google.cn/guide/components/processes-and-threads?hl=zh-cn#java
在这里插入图片描述

Android UI操作并非线程安全。因此,请不要在工作线程(即子线程)中操纵界面。您可以通过界面线程对界面进行所有操作。Android 的单线程模型有以下两条规则:

  • 请勿阻塞UI线程
  • 请勿从UI线程以外的线程进行UI操作

为什么要设计Handler机制?

一个耗时的操作,比如需要联网读取数据或者读取本地较大的一个文件或者数据库查询,如果把这些操作放在主线程中,界面会出现假死现象, 如果5秒钟还没有完成的话,阻塞了UI线程会收到Android系统的一个错误提示 “强制关闭”,这糟糕的体验会造成严重的损失,所以不能阻塞UI线程,以确保应用界面的响应能力。
这时候就需要把这些耗时的操作,放到子线程中去执行,但是在子线程执行完以后,又需要将结果更新到UI页面,此时就涉及到子线程UI更新的操作。那为什么安卓规定不能在子线更新UI? 最根本的原因是多线程并发的问题,假设在一个Activity中,有多个线程去更新UI,并且都没有加锁机制,就会产生更新界面错乱,所以子线程中更新UI是不安全的,而在一个线程中更新UI是变得比较合理,那自然就是主线程了,当然主线程也可以叫UI线程了。
综上所述,所以安卓应用需要这样的机制:

  1. 只能在主线程更新UI,并且所有更新UI的操作,都要在主线程的消息队列当中去轮询处理。
  2. 耗时的操作在子线程,更新UI的操作在主线程,他们之间的交互需要实现线程间通信。

Handler用于实现多线程通信和管理UI线程的消息处理。它为开发者提供了一种简单有效的方法来处理异步任务和更新UI界面。
当然,Android已经提供了多种从其他线程访问UI线程的方式。以下列出了几种有用的方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • Handler.post(Runnable)

Activity.runOnUiThread(Runnable)方法:如果在UI线程,直接更新UI;如果非UI线程,使用的是post。

new Thread(new Runnable() {
    @Override
    public void run() {
        // todo 在子线程中进行耗时操作
        doSomethings();
        
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // todo UI更新代码
                doSomethingsAboutUI();
            }
        });
    }
}).start();

View.post(Runnable) 方法:

new Thread(new Runnable() {
    @Override
    public void run() {
		// todo 在子线程中进行耗时操作
        doSomethings();
		
        findViewById(R.id.button_send).post(new Runnable() {
            @Override
            public void run() {
                // todo UI更新代码
                doSomethingsAboutUI();
            }
        });
    }
}).start();

Handler.post(Runnable)方法

private Handler mHandler =new Handler();  //默认主线程
...
new Thread(new Runnable() {
    @Override
    public void run() {
        // todo 在子线程中进行耗时操作
        doSomethings();
        
        mHandlers.post(new Runnable() {
            @Override
            public void run() {
                // todo UI更新代码
                doSomethingsAboutUI();
            }
        });
    }
}).start();

随着操作变得越来越复杂,这种代码也会变得复杂且难以维护。为了处理与工作线程(子线程)的更复杂的交互,建议在UI线程中使用 Handler 处理从子线程传送的消息。

工作原理:Handler运行在主线程(UI线程)中, 它与子线程可以通过Message对象来传递数据和消息,然后把这些消息放入主线程队列中,按照先进先出的原则配合主线程逐个进行更新UI的操作。另外,驱动这套机制运行的核心是Looper.loop() 里的死循环。

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare(); // 创建Looper的子线程,然后创建MessageQueue,最后进行绑定。
    synchronized (this) {
        mLooper = Looper.myLooper();  // 获取准备好的Looper对象
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();  // 启动Looper
    mTid = -1;
}

Handler的用法

下面的这种写法是可以实现刷新UI的功能,但是它违背了单线程模型:Android UI操作并不是线程安全的,并且这些操作必须在UI线程中执行。但是如果与UI无关的操作如上传/下载,数据库,可以使用此种写法。

new Thread(new Runnable() {
    @Override
    public void run() {
        textviewCurrentStatus.invalidate();
    }
}).start();

优点:避免了创建新线程带来的线程切换开销。
缺点:Handler发送的消息会保存在消息队列中,如果一直发送大量的消息,将可能导致消息队列过长,影响应用的响应能力。LiveData和RxJava等现在比较流行的框架,能够替代Handler实现更优异的异步编程和UI更新。

1、创建Handler

一般在主线程中创建Handler如下:

private Handler mHandler = new Handler(Looper.getMainLooper()) {
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                // todo 在主线程更新UI
                doSomethingsAboutUI();
                break;
        }
        super.handleMessage(msg);
    }
};

在子线程中创建Handler,最好使用HandlerThread。如果不使用HandlerThread,必须要手动启动Looper,具体如下:

private Handler mHandler;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    new Thread(new Runnable() {
        @Override
        public void run() {
        	// todo 在子线程进行耗时操作
        	doSomethings();
        	
            Looper.prepare();  // 准备Looper
            mHandler= new Handler(new Handler.Callback() {
                @Override
                public boolean handleMessage(@NonNull Message msg) {
                	// todo 实际还是在主线程更新UI
                	doSomethingsAboutUI();
                    return false;
                }
            });
            Looper.loop();    // 启动Looper
        }
    }).start();
}

2、Handler通信

使用Handler通信,有两种方法将消息加入消息队列中:post()方法和sendMessage()方法。
– sendMessage()方法是异步方式。即加入消息到消息队列中后,不会立即执行此消息,而是等待消息阻塞的处理程序返回。 — 存疑
– post()方法是同步方式。即加入消息到消息队列中后,会直接处理此消息,不必等待消息阻塞的处理程序返回。— 存疑

2.1 sendMessage 方式

首先,需要定义好handler需要处理的业务。

private Handler myHandler = new Handler(Looper.getMainLooper()) {
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                // todo 在主线程更新UI
                doSomethingsAboutUI();
                break;
        }
        super.handleMessage(msg);
    }
};

在需要的时机,发送消息触发handler调用业务。

new Thread(new Runnable() {
    @Override
    public void run() {
        // todo 在子线程中进行耗时操作
        doSomethings();
        
        Message message = new Message(); //或者Message msg = mHandler.obtainMessage();
        message.what = 1;
        msg.arg1 = 100;
		msg.obj = "message content";
        myHandler.sendMessage(message);
    }
}).start();

另外一种常见写法,本质都是一样的:

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        switch (msg.what){
            case 1:
                // todo 在主线程更新UI
                doSomethingsAboutUI();
                break;
        }
        return false;
    }
});
....
new Thread(new Runnable() {
     @Override
     public void run() {
         // todo 在子线程中进行耗时操作
         doSomethings();

        Message message = mHandler.obtainMessage(1);
        mHandler.sendMessage(message);
    }
}).start();

2.2 post 方式

通过调用 Handler 的 post() 方法,将 Runnable 对象通过 MessageQueue 发送到消息队列中,即可让主线程处理相应的操作。这种方式可以用于解决在子线程中不能进行 UI 操作的问题,例如我们可以在子线程中通过 post 方式将更新 UI 的任务传递到主线程来完成,这样就不会因为在非 UI 线程中更新 UI 而导致 ANR(Application Not Responding)了。

private Handler mHandler =new Handler();
...
mHandler.post(new Runnable() {
    @Override
    public void run() {
        // todo UI更新代码
        doSomethingsAboutUI();
    }
});

注意:post方法虽然发送的是一个实现了Runnable接口的类对象,但是它并非创建了一个新线程,而是执行了该对象中的run方法。也就是说,整个run中的操作和主线程处于同一个线程。这样对于那些简单的操作,似乎并不会影响。但是对于耗时较长的操作,就会出现“假死”。为了解决这个问题,就需要使得handler绑定到一个新开启线程的消息队列上,在这个处于另外线程的上的消息队列中处理传过来的Runnable对象和消息。

在主线程中使用Handler,可以直接使用getMainLooper()获取主线程Looper对象,并创建Handler实例。例如,在Activity中实现在子线程中更新UI:

private Handler mHandler = new Handler(Looper.getMainLooper());
...
new Thread(new Runnable() {
    @Override
    public void run() {
        // todo 在子线程中进行耗时操作
        doSomething();
        
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                // todo 在主线程更新UI
                doSomethingsAboutUI();
            }
        });
    }
}).start();

Handler常用方法

1、延时执行

3秒后执行UI更新的代码。

mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
        // todo UI更新代码
        doSomethingsAboutUI();
    }
},3000);

2、周期执行

有时候需要按时反复周期性的执行一些任务
2.1 使用Timer和TimerTask 实现

private Timer mTimer = new Timer();
private TimerTask mTimerTask = new TimerTask() {
    @Override
    public void run() {
        Message message = new Message();
        message.what = 1;
        mHandler.sendMessage(message);
    }
};

private Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                // todo 处理定时或周期性的业务
                doSomethings();
                
                break;
        }
        super.handleMessage(msg);
    }
};

在需要触发的时机,调用即可

mTimer.schedule(mTimerTask, 10000);

2.2 使用postDelayed和sendMessage实现

private int index = 0;

private Handler mHandler = new Handler(Looper.getMainLooper()) {
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                updateOnTick();
                break;
        }
        super.handleMessage(msg);
    }
};

private void updateOnTick(){
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            // todo 周期性执行
            doSomethings();
            Log.d(TAG, "run: " + (index++));
            mHandler.sendMessage(Message.obtain(mHandler,1));
        }
    }, 2000);  // 2秒执行一次
}

HandlerThread用法

HandlerThread常用于需要在后台执行耗时任务,并与UI线程进行交互的场景。比如,每隔6秒需要切换一下TextView的显示数据,虽然可以在UI线程中执行,但是这样的操作长时间占用UI线程,很容易让UI线程卡顿甚至崩溃,所以最好在子线程HandlerThread中调用这种业务。
HandlerThread能新建一个拥有Looper的线程。这个Looper能够用来新建其他的Handler。但需要注意的是,新建的HandlerThread需要及时回收,否则容易内存泄露。

非UI线程的业务也可以使用HandlerThread消息机制,因为不会干扰或阻塞UI线程,而且通过消息可以多次重复使用当前线程,也可以多个Handler也可以共享一个Looper,节省开支。

一个线程只能创建一个Looper,一个Looper个创建多个Handler。

使用HandlerThread可以实现以下功能和优势:

  1. 后台线程执行任务:HandlerThread在后台创建一个工作线程,可以在该线程中执行耗时任务,而不会阻塞UI线程,保证了应用的响应性和流畅性。
  2. 消息处理和线程间通信:HandlerThread内部封装了Looper和Handler,可以轻松地实现消息的发送和处理,以及线程间的通信。通过HandlerThread,可以将耗时任务的结果发送到UI线程进行更新,或者接收UI线程发送的消息进行处理。
  3. 简化线程管理:HandlerThread将线程的创建和管理进行了封装,开发人员只需要关注业务逻辑的实现,而无需手动创建和管理线程,减少了线程管理的复杂性。

主线程-创建Handler

private Handler mHandler;
private HandlerThread mHandlerThread;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mHandlerThread = new HandlerThread("子线程HandlerThread");
    mHandlerThread.start();
    mHandler= new Handler(mHandlerThread.getLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 1:
                    Log.d(TAG, "handleMessage: " + mHandlerThread.getName());
                    // todo 在主线程更新UI
                    doSomethingsAboutUI();
                    break;
            }
        }
    };
}
...
private void sendMessages(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            // todo 在子线程中进行耗时操作
            doSomethings();

            Message message = mHandler.obtainMessage(1);
            mHandler.sendMessage(message);
        }
    }).start();
}

@Override
protected void onPause() {
    super.onPause();
    // 防止退出界面后Handler还在执行
    mHandler.removeMessages(1);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    // 防止退出界面后Handler还在执行
    mHandler.removeMessages(1);
    // 释放资源
    mHandlerThread.quit();
}

子线程-创建Handler

private Handler mHandler;
private HandlerThread mHandlerThread;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
	new Thread(new Runnable() {
        @Override
        public void run() {
            mHandlerThread = new HandlerThread("子线程HandlerThread");
            mHandlerThread.start();
            mHandler= new Handler(mHandlerThread.getLooper()) {
                @Override
                public void handleMessage(@NonNull Message msg) {
                    super.handleMessage(msg);
                    switch (msg.what){
                        case 1:
                            Log.d(TAG, "handleMessage: " + mHandlerThread.getName());
                            // todo 在主线程更新UI
                            doSomethingsAboutUI();
                            break;
                    }
                }
            };
        }
    }).start();
}
...
private void sendMessages(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            // todo 在子线程中进行耗时操作
            doSomethings();
			Log.d(TAG, "sendMessages: 耗时操作");
			
            Message message = mHandler.obtainMessage(1);
            mHandler.sendMessage(message);
        }
    }).start();
}

@Override
protected void onPause() {
    super.onPause();
    // 防止退出界面后Handler还在执行
    mHandler.removeMessages(1);  // 删除所有消息 mHandler.removeCallbacksAndMessages(null);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    // 防止退出界面后Handler还在执行
    mHandler.removeMessages(1);   // 删除所有消息 mHandler.removeCallbacksAndMessages(null);
    // 释放资源
    mHandlerThread.quit();
}

注意:1、子线程中创建了Looper,当没有消息的时候子线程将会被block,无法被回收,所以我们需要手动调用quit 方法将消息删除并且唤醒looper,然后next方法返回null退出loop。
2、在主线程和子线程中,使用HandlerThread创建Handler,基本没有区别。但如果没有使用HandlerThread,在子线程中需要先创建Looper,再创建Handler。具体如下:

private Handler mHandler ;
...
new Thread(new Runnable() {
   @Override
   public void run() {
       Looper.prepare(); // 创建Looper的子线程,然后创建MessageQueue,最后进行绑定。
       mHandler = new Handler(new Handler.Callback() {
           @Override
           public boolean handleMessage(@NonNull Message msg) {
               return false;
           }
       });
       Looper.loop();  // 启动Looper
   }
}).start();

FAQ

Message是如何创建

首先考虑一个问题,屏幕刷新率 60Hz(即每秒刷新60次),每次刷新要用到 3 个 Message,也就是每秒钟至少要创建180个Message。这样不断的创建回收,就会出现内存抖动的问题,从而导致 GC、屏幕卡顿等问题。
为了解决上面的问题,采用 Message 了享元的设计模式,使用 obtain() 方法创建。在Handler 中创建两个线程池队列,一个是我们比较熟悉的 MessageQueue,另一个就是回收池 sPool(最大长度是 50 复用池)。MessageQueue 中 Message 回收时,我们将清空数据的 Message 放回到 sPool 队列中。创建 Manager,我们直接从 sPool 池中取出来就可以了。
应用场景:地图、股票、RecyclerView复用等对数据的处理都使用了享元模式。

主线程中Looper的轮询死循环为何没有阻塞主线程

Looper 轮询是死循环,但是当没有消息的时候他会 block(阻塞, 阻塞代码没有执行计时操作),ANR 是当我们处理点击事件的时候 5s 内没有响应,我们在处理点击事件的时候也是用的 Handler,所以一定会有消息执行,并且 ANR 也会发送 Handler 消息,所以不会阻塞主线程。Looper 是通过 Linux 系统的 epoll 实现的阻塞式等待消息执行(有消息执行无消息阻塞),而 ANR 是消息处理耗时太长导致无法执行剩下需要被执行的消息触发了 ANR。Handler底层为什么用epoll,为什么不用select和poll? Socket 非阻塞 IO 中 select 需要全部轮询不适合,poll 也是需要遍历和 copy,效率太低了。epoll 非阻塞式 IO,使用句柄获取 APP 对象,epoll 无遍历,无拷贝。还使用了红黑树(解决查询慢的问题)。

Handler内存泄漏问题及解决方案

内部类持有外部类的引用导致了内存泄漏,如果 Activity 退出的时候,MessageQueue中还有一个 Message 没有执行,这个 Message 持有了 Handler 的引用,而 Handler 持有了 Activity 的引用,导致 Activity 无法被回收,导致内存泄漏。这种问题可以使用 static 关键字修饰,在 onDestory 的时候将消息清除。
简单理解:当 Handler 为非静态内部类时,其持有外部类 Activity 对象,所以导致 static Looper -> mMainLooper -> MessageQueue -> Message -> Handler -> MainActivity,这个引用链中 Message 如果还在 MessageQueue 中等待执行,则会导致 Activity 一直无法被释放和回收。
根本原因:因为Looper需要循环处理消息,但一个线程只有一个Looper,而一个线程中可以有多个Handler,MessageQueue中消息Message 执行时不知道要通知哪个Handler执行任务,所以在Message创建时target引用了Handler对象,用于回调执行的消息。
如果Handler是Activity这种短生命周期对象的非静态内部类时,则创建出来的Handler对象会持有该外部类Activity的引用,当页面销毁时,还在队列的Message持有着Handler对象,而Handler正持有着外部类Activity,就会导致 Activity无法被gc回收,从而导致内存泄漏。
解决办法:
1、Handler不能是Activity这种短生命周期的对象类的内部类;
2、在 Activity销毁时,将创建的 Handler中的消息队列清空并结束所有任务。
3、将handler设置成static,static变量是全局变量,不能够自动引用外部类变量,这时Handler 就不再持有 Activity,Activity就可以正常释放。

Handler为什么会持有Activity的引用?

创建Handler时,采用的是匿名内部类或者成员内部类的方式,而内部类会默认持有外部类的引用,也就是Handler对象会默认持有Activity的引用。