在上一篇文章末尾我们提到了——在子线程中使用Toast需要做以下处理:
newThread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(getApplicationContext(), "子线程显示", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
根据我们的标题,除非Toast使用到了Handler通信机制,否则在子线程中使用toast,不必调用Looper.prepare()和Looper.loop()。
那Toast到底有没有使用到Handler通信机制呢?我们从使用Toast的步骤,结合源码一步步的来分析。
步骤一、Toast
步骤二、makeText(getApplicationContext(), "子线程显示", Toast.LENGTH_SHORT)
步骤三、.show();
####首先,我们看步骤一Toast涉及到的源码:
Toast的构造方法有以下几个:
#####一、
public Toast(Context context) {
this(context, null);
}
这个是我们通过关键字new得到一个Toast对象时使用的方法,但这里并没有与handler扯上联系。
#####二、
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(com.android.internal.R.integer.config_toastDefaultGravity);
}
这里构造方法里面就要去我传递一个Looper对象的参数,到这里我们初步获得了Toast跟handler有关的信息。但不急,我们继续看源码,看看这里Looper对象或者handler对象的作用是什么,是不是通信。
在源码里面我们看到Looper对象传递进去后就没有在下一步的操作,而与此有关联的对象是mTN,这里从源码上看,这里是在设置mTN的位置和尺寸大小。到此我们基本可以判定,Toast的构造方法里面handler对象没有起到通信的作用。
####然后,我们再看看步骤二makeText(Context context, CharSequence text, Duration int duration)涉及到的源码
步骤二makeText的源码分三部分,但三个方法都是层层的调用,最终归集到一个方法上来:
方法1、
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) throws Resources.NotFoundException {
return makeText(context, context.getResources().getText(resId), duration);
}
该方法我们看不出什么端倪,也看不到与handler有什么关系,直接的反应是他调用了makeText()的方法,那我们继续看他调用的makeText()方法。
方法2:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
感觉很扯蛋吧,这里也是没什么有用的事情,还是直接调用了另一个makeText()方法。好吧,我们再看看他调用的makeText()方法:
方法3:
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;
}
这里我们终于看到了干了点实事的代码。
首先,从参数上看,涉及到了Looper对象,但是前面的方法1压根儿就没有传Looper参数过来,方法二传了一个空对象过来,所以这里依然不涉及到handler的实际上的使用。
然后,从代码上看,这里是在布局并渲染Toast的UI界面,设置相关的参数。
因此步骤二也不涉及到handler的实际使用。
####最后我们再来看看步骤三show()方法做了什么。
源码如下:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
这里,很奇怪我的源码里面INotificationManager、getOpPackageName()、enqueueToast()三处爆红,无法进一步看到内部具体实现。
无法看到具体实现,那我们先看看这个方法做了些什么事情。
这里,先是获取INotificationManager的服务对象,然后是获取到当前应用的包名,再是两个赋值操作。貌似也没什么特别的地方。最后是try…catch下的enqueueToast(pkg, tn, mDuration)操作。
等等,等等,enqueueToast()?是不是有点熟悉?enqueue?入队的意思?enqueueToast?吐司入队?但遗憾,由于爆红的缘故,无法查看到enqueueToast()方法的源码。
但发现另外一个很有趣的事情,那就是第一步和这一步都不断出现的一个参数:mTN。我们知道,在handler的入队操作的时候是要传入一个handler对象的,这里进行enqueueToast()操作使用到了mTN参数,莫非mTN就是handler或者是一个与handler有关的参数?
走吧,去瞧瞧他的构成。mTN的是TN.class的对象。他的源码不多,具体如下:
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
private static final int SHOW = 0;
private static final int HIDE = 1;
private static final int CANCEL = 2;
final Handler mHandler;
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
String mPackageName;
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
TN(String packageName, @Nullable Looper looper) {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
public void handleShow(IBinder windowToken) {
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to
// announce a transient piece of information to the user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
}
在这里面,我们很容易的发现了Handler的存在,以及handler对象相关的方法:SHOW、HIDE、CANCEL等信息,我们简单的看下,不扯太多把子。
show()方法:
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
}
hide()方法:
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
}
cancel()方法:
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
}
这里三个方法里面,都调用了mHandler.obtainMessage()方法,查源码可以发现:
public final Message obtainMessage(int what, Object obj) {
return Message.obtain(this, what, obj);
}
这里调用的是Handler下四大金刚之Message的obtain()方法:
public static Message obtain(Handler h, int what, Object obj) {
Message m = obtain();
m.target = h;
m.what = what;
m.obj = obj;
return m;
}
其作用是封装消息进入Message对象。然后我们再来看后面的sendToTarget()方法:
public void sendToTarget() {
target.sendMessage(this);
}
通过前面两篇关于Handler文章的分析,我们知道这里的target其实就是handler对象,他调用sendMessage方法,该方法先调用sendMessageDelayed()方法,然后sendMessageDelayed()方法调用sendMessageAtTime()方法,sendMessageAtTime()方法最终调用enqueueMessage()方法,将我们要发送的吐司消息入队。至此,我们完全获得了Toast通知使用Handler的过程。
由该过程我们也清除的了解到为什么我们在子线程里面使用Toast,需要先后调用Looper.prepare()和Looper.loop()方法。因为Toast通知在本质上是利用Handler将消息发送到UI线程然后刷新UI。其本质上跟我们的前一篇文章《Handler消息机制二——子线程下如何使用Handler》是一样的。
真相明了,不再累述。