首先引入几个问题:
- Handerl如何避免内存泄漏
- 更新view,难道只能在UI线程吗?子线程不可以吗?
- 子线程中为什么不能创建Handler
- new Handler的两种方式区别
- ThreadLocal用法及原理
一、Handerl如何避免内存泄漏
1、首先写一段内存泄漏的代码,如下:
public class HandlerActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread(new Runnable() {
@Override
public void run() {
Message msg = new Message();
msg.what = 1;
msg.obj = "qb";
//延迟2s发送消息
SystemClock.sleep(2000);
mHandler.sendMessage(msg);
}
}).start();
}
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//处理消息 跳转到另外一个界面
Log.e("HandlerActivity销毁了销毁了", "接收到消息,仍然跳转");
startActivity(new Intent(HandlerActivity.this, SecondActivity.class));
return false;
}
});
@Override
protected void onDestroy() {
super.onDestroy();
Log.e("onDestroy》》》", "HandlerActivity销毁了");
}
}
创建一个子线程,在子线程中延迟2s,发送消息。handler接收到消息跳转到另外一个界面。这里我们只需要在2s内推出当前app。等待2s结束后,发现app重新启动,并跳转至第二个界面。说明发生了内存泄漏,运行结果截图如下:
那么如何避免这种内存泄漏呢?
2、解决内存泄漏
(1)尝试在onDestroy方法中移除消息,代码如下:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeMessages(1);
Log.e("onDestroy》》》", "HandlerActivity销毁了");
}
运行发现没有效果,这是因为在销毁的时候我们的线程仍在休眠,这个消息并没有压入队列中,所以我们移除的为空。即移除之前并没有调用sendMessage相关方法。
(2)使用sendMessageDelay方法代替sendMessage,onDestroy()方法处理如上。局部代码如下:
new Thread(new Runnable() {
@Override
public void run() {
Message msg = new Message();
msg.what = 1;
msg.obj = "qb";
//延迟2s发送消息
// SystemClock.sleep(2000);
//mHandler.sendMessage(msg);
mHandler.sendMessageDelayed(msg,2000);
}
}).start();
调用该方法会把消息压入到队列,并延迟2s进行唤醒。那么在onDestroy方法中就可以将该消息进行移除。
(3)如果强行使用sendMessage()方法,可以使用如下解决方法:
new Thread(new Runnable() {
@Override
public void run() {
Message msg = new Message();
msg.what = 1;
msg.obj = "qb";
//延迟2s发送消息
SystemClock.sleep(2000);
if(mHandler != null) mHandler.sendMessage(msg);
}
}).start();
@Override
protected void onDestroy() {
super.onDestroy();
//这一行加不加都可以
mHandler.removeMessages(1);
mHandler = null;
Log.e("onDestroy》》》", "HandlerActivity销毁了");
}
只需要在onDestroy方法中将handler置空,并且线程中加入判断即可。这样当界面销毁的时候就不会将消息加入到消息队列。
(4)尝试2,是否可以定义全局Message,在onDestory方法中回收message?代码如下:
msg = new Message();
new Thread(new Runnable() {
@Override
public void run() {
msg.what = 1;
msg.obj = "qb";
//延迟2s发送消息
SystemClock.sleep(2000);
mHandler.sendMessage(msg);
//if(mHandler != null) mHandler.sendMessageDelayed(msg,2000);
}
}).start();
@Override
protected void onDestroy() {
super.onDestroy();
//这一行加不加都可以
//mHandler.removeMessages(1);
//mHandler = null;
Log.e("onDestroy》》》", "HandlerActivity销毁了");
msg.recycle();
}
运行结果截图如下:
这样虽然解决了跳转的问题,但是抛出了异常"This message is already in use."但是这种方式仅在调用sendMessageDelay()方法不会抛出这种异常,具有局限性。不推荐使用。
(5)使用handler.removeCallbacksAndMessages(null)。就是在Acticity退出时最好调用handler.removeCallbacksAndMessages(null),移除handler的所有消息,避免内存泄漏。记住调用handler.removeCallbacksAndMessages(null)只会移除当前handler的所有消息,如何存在多个handler,需要每一个handler都调用一次。
二、更新view,难道只能在UI线程吗?子线程不可以吗?
1、同样写一段测试代码:
public class HandlerActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
mTextView = findViewById(R.id.test_tv);
mTextView.setText("1111111111");
new Thread(new Runnable() {
@Override
public void run() {
mTextView.setText("qbbbbbbb");
}
}).start();
}
}
在子线程中直接更新textView。运行发现界面更新成功了,但是如果我们在子线程弹出toast,就会抛出异常,截图如下:
同样的,我们可以在更新textView之前加上线程休眠的代码,发现抛出了异常,截图如下:
查看setText()源码。跟踪方法顺序setText()-->...-->checkForRelayout()-->[ requestLayout();invalidate();] 先跟踪requestLayout()方法。最终会调用ViewRootImpl中的requestLayout()方法,代码如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
最总会调用checkThread()方法,检查当前线程如果不是在主线程,就会抛出异常。对于invalidate()方法,它是通知界面刷新方法。也就是说如果当前ui刷新所在的线程和view的创建线程在同一个线程,校验通过。反之,就会抛出异常。这里延迟2s,UI已经加载到窗口,是在主线程中,2s后,发现更新在子线程,所以会抛出异常。这里可以尝试在onCreate()和onResume()方法中不进行延迟2s的更新(没有问题),在onStop()方法中再次更新(发现有问题),根本原因,还是checkThread()发生在布局加载完成后,才进行校验。代码如下:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
所以,更新view,不一定在主线程中。子线程中满足一定条件也可以进行界面刷新。
三、子线程中为什么不能直接创建Handler
1、同样写一段测试代码,如下:
new Thread(new Runnable() {
@Override
public void run() {
new Handler();
}
}).start();
运行保存,如下截图:
2、跟踪源码进入Handler的构造方法,最终会进入两个参数的构造方法,源码如下:
不难看到,当mLooper==null的时候就会发生上面的异常。那么mLooper从何而来,调用Looper.myLooper()方法。代码如下:
这里的threadLocal后面会讲到,它其实是以键值对的形式存储对象。其中key为当前handler所在线程,value为Looper对象。并且threadLocal是全局唯一的。它的get方法其实是从theadLocalMap中获取Looper对象。在上面的写法中,显然子线程的looper是没有存储到threadLocalMap中的。这也就验证了在子线程找那个是不能直接new Handler()的。
四、new Handler的两种方式区别
1、写一段测试代码,如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
mTextView = findViewById(R.id.test_tv);
mTextView.setText("1111111111");
//方式1
Handler handler1 = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
//方式2
Handler handler2 = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
return false;
}
});
}
不难发现,两种方式最终消息处理的地方都在handleMessage()中。我们可以通过源码跟踪。进入Handler.class类中,消息队列取出消息以后,交给handler处理的方法dispatchMessage()方法,代码如下:
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
2、if里面的msg.callback其实就是runnable接口(调用handler.post方法),这里先不关心,在else里面,如果我们使用第二种方式那么mCallback(接口)就不为空,就会执行回调方法(mCallback.handleMessage)。否则执行当前类的备用方法handleMessage。这里推荐使用接口的方法。贴上一段回调接口,和备用方法源码:
/**
* Callback interface you can use when instantiating a Handler to avoid
* having to implement your own subclass of Handler.
*/
public interface Callback {
/**
* @param msg A {@link android.os.Message Message} object
* @return True if no further handling is desired
*/
public boolean handleMessage(Message msg);
}
/**
* Subclasses must implement this to receive messages.
*/
public void handleMessage(Message msg) {
}
五、ThreadLocal用法及原理
1、写一段单元测试代码,如下:
public class TreadLocalTest {
@Test
public void threadLocalTest(){
//初始哈threadLocal,键为主线程,值默认值为"初始值"
final ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "初始值";
}
};
System.out.printf("当前mainThread线程--值:%s\n",threadLocal.get());
threadLocal.set("主线程");
System.out.printf("当前mainThread线程--set值:%s\n",threadLocal.get());
//子线程thread-1
new Thread(new Runnable() {
@Override
public void run() {
System.out.printf("当前子线程thread-1--值:%s\n",threadLocal.get());
threadLocal.set("子线程1");
System.out.printf("当前子线程thread-1--set值:%s\n",threadLocal.get());
}
}).start();
//子线程thread-2
new Thread(new Runnable() {
@Override
public void run() {
System.out.printf("当前子线程thread-2--值:%s\n",threadLocal.get());
threadLocal.set("子线程2");
System.out.printf("当前子线程thread-2--set值:%s\n",threadLocal.get());
}
}).start();
}
}
一开始我在主线程中创建了一个threadLocal对象,并且重写了它的initialValue()方法。这里创建的threadLocal对象存放的key是当前线程,value是字符串。执行以上代码,结果如下:
2、分析结果
在主线程中创建threadLocal,调用threadLocal.set()方法,此时key为mainThread,value将会被替换成"主线程"。在子线程中一开始直接调用get方法,发现打印的值为initvaule()返回的值,为什么不是mainThread开始set的值呢?这是因为在子线程中,threadLocal中key为thread-1或thread-2。一开始get是无法取到的,所以返回默认值。当我们重新set以后才可以取到新的值。所以谨记一点就是threadlocal的key永远是当前线程。
3、源码分析
找到threadLocal.get()方法,源码如下:
public T get() {
//获取当前所在线程
Thread t = Thread.currentThread();
//获取当前线程的threadLocalMap
ThreadLocalMap map = getMap(t);
//不为空的话从map中取出当前线程的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//为空的话返回initValue值
return setInitialValue();
}
private T setInitialValue() {
//返回initialValue初始值
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
获取当前线程的threadLocalMap
ThreadLocalMap map = getMap(t);
//如果不为空直接设置当前线程的默认值为value 否则创建map
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
不难发现无论是主线程还是子线程,一开始threadLocal一定是为空的。那么最终都会调用setInitialValue()方法,将默认值放到map中,并返回默认值,这也就解释了每个线程一开始调用get方法都会返回"初始值"。如果threaLocal不为空就会直接将原来的value值替换成现在的,放到map中。再看一些threaLocal.get()方法,代码如下:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的threadLocalMap
ThreadLocalMap map = getMap(t);
//map如果不为空就替换成新的value值 ,为空创建新的map并setValue值
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
当当前线程的threadLocalMap创建成功以后,执行set方法以后会替换新的值,这也就解释了当再次调用set方法以后,get出来的就是当前的最新值。