由Android Toast 到 ThreadLocal的思考
在Android开发中又是我们试图这样调用代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Thread thread = new Thread(){
@Override
public void run() {
/**
* 这里执行一些耗时的操作,然后拿到最终的结果数据
* ...$@%%#@@*(*&...
*/
Toast.makeText(getApplicationContext(), "加载数据成功!", Toast.LENGTH_SHORT).show();
}
};
thread.start();
}
}
结果崩溃了,然后打印出这样的log
我去我显示一个toast跟handler有毛线关系啊(哎,还是太年轻啊),看看Toast源码,发现有这样的代码被调用
final Handler mHandler = new Handler();
那又怎样,怎么有这样的代码被调用有为啥会崩溃呢(一脸懵逼的样子),很多人可能就知道,不过我还得说说。再看看Handler里面的源码
(哼着小曲:我尋尋覓覓尋尋覓覓 一個溫暖的懷抱 這樣的要求算不算太高...)
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
//这里是报错的关键
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
唱着小曲我找到了问题的关键(看来以后还得多多哼小蛐蛐),在上面代码的12行到16行,对比一下上面出现的错误(没错就是他,就是他吻了我,呜呜….),
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
可以看到调用Looper.myLooper()
返回一个 null的时候就报错了(这又是为啥呢,难道我长得好看,就可以随意的让被人亲吗… ),然后再进去看看里面的代码实现
(哼着小曲:我尋尋覓覓尋尋覓覓 一個溫暖的懷抱 這樣的要求算不算太高…)
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
这里调用了sThreadLocal.get();
既然有get那么理应有个set才能服众啊,果然发现了在,Looper里面发现了这个set
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
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));
}
共有方法prepare()
里面调用了静态私有的prepare(boolean quitAllowed)
方法,看来我们找到解决方法了,那就是调用一下Looper.prepare()
方法(恩恩, 我终于可以打那个亲我的那个男孩了,嘻嘻)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Thread thread = new Thread(){
@Override
public void run() {
Looper.prepare();
/**
* 这里执行一些耗时的操作,然后拿到最终的结果数据
* ...$@%%#@@*(*&...
*/
Toast.makeText(getApplicationContext(), "加载数据成功!", Toast.LENGTH_SHORT).show();
Looper.loop();
}
};
thread.start();
}
}
(报仇的感觉真爽,下次我还想让他亲我,然后我再打他,哈哈)不过这里,第16行添加了一行Looper.loop();
的代码,这里稍后解释。
整个问题的产生源于Toast里面用到了Handler,而Handler用到了Looper,那为啥Toast要使用到Handler,可以看到Toast源码里面一个内部类NT(进程回调类)使用到了
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
可知这里只是使用到了Handler的消息循环的机制(自己发送消息,然后自己处理消息)。当然这里就需要使用到Looper,所以需要调用Looper.prepare()
(为当前线程设置一个Looper对象)以及调用Looper.loop()
方法(使得整个消息可以循环起来)。假设我们这样调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Thread thread = new Thread(){
@Override
public void run() {
Looper.prepare();
Looper.prepare();
/**
* 这里执行一些耗时的操作,然后拿到最终的结果数据
* ...$@%%#@@*(*&...
*/
Toast.makeText(getApplicationContext(), "加载数据成功!", Toast.LENGTH_SHORT).show();
Looper.loop();
}
};
thread.start();
}
会报错
FATAL EXCEPTION: Thread-2456
Process: com.example.imagetouch, PID: 10321
java.lang.RuntimeException: Only one Looper may be created per thread
at android.os.Looper.prepare(Looper.java:77)
at android.os.Looper.prepare(Looper.java:72)
at com.example.imagetouch.MainActivity$1.run(MainActivity.java:23)
Only one Looper may be created per thread
有点熟悉,这个就是上面代码列出的Looper.prepare(boolean quitAllowed)
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));
}
如果sThreadLocal.get() != null
就会报错,也就是说,如果当前的线程有了一个Looper,再调用Looper.prepare()之后就会报错。这样做的目的就是实现每一个线程只有一个Looper对象,实现这样的功能就是依靠ThreadLocal这个类了。
这个可以参考理解Java中的ThreadLocal写的很好,
总结来说就是:
每一个Android Thread类里面都有一个
ThreadLocal.Values localValues;
成员变量(java里面是ThreadLocal.ThreadLocalMap threadLocals
,作用差别不大),它保存了ThreadLocal
的值,我们这个事例中保存的就是Looper这个对象。也就是说ThreadLocal
本身是不会保存这个值的,他只是提供了get
(获取设置的值)跟set
(设置一个值进来)这两个方法,这两个方法做得事情就是将Thread作为一个key,设置进来的值作为一个value,以键值对的形式保存(或者取出来)在Thread 的 ThreadLocal.Values成员变量中,而这个ThreadLocal.Values是ThreadLocal的内部类。
参考资料:
http://lavasoft.blog.51cto.com/62575/51926
http://www.iteye.com/topic/103804
http://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/index.html
http://alighters.com/blog/2016/06/25/threadlocal-in-android-message/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io