在子線程中不能直接調(diào)用Toast.makeText(this,title,Toast.LENGTH_SHORT).show();需要放在主線程里面反症。原因我們后面會講梢褐。因此帆谍,我將Toast放在了runOnUiThread方法中(此方法寫在一個我測試的Activity里面)饼酿。
public void shotToast(final String title) {
new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(this,title,Toast.LENGTH_SHORT).show();
}
});
}
}).start();
}
但是這段話編譯出錯了运准。錯在哪里了呢吭敢?就錯在makeText的第一個參數(shù)上面碰凶。我這java基礎(chǔ)真不扎實呀。Activity中的匿名類或者內(nèi)部類中不能直接用this鹿驼,因為此時this指的是這個內(nèi)部類欲低,所以要訪問外部類的Activity就要用Activity.this,如果不是在內(nèi)部類中畜晰,就直接用this砾莱。
我們還可以把上面的this改成getBaseContext()。點進getBaseContext()方法看到這個方法在ContextWrapper中是這樣的:
/**
* @return the base context as set by the constructor or setBaseContext
*/
public Context getBaseContext() {
return mBase;
}
然后我就疑惑凄鼻,mBase就一定有值嗎腊瑟?
看過我寫的另一篇文章Context及其子類源碼分析(一)會知道Activity繼承自ContextThemeWrapper,ContextThemeWrapper繼承自ContextWrapper块蚌。ContextWrapper持有一個抽象構(gòu)件的引用闰非,就是代碼里的mBase,真正的ContextImpl的實例會在Activity實例被創(chuàng)建后被創(chuàng)建峭范,然后通過Activity的attach方法賦給mBase财松。所以正常ContextImpl的實例被創(chuàng)建后,mBase是有值的纱控。
上面我們講了兩個事實:
1辆毡、匿名類或內(nèi)部類中用this指的是這個匿名類或內(nèi)部類。
2甜害、Activity中ContextImpl的實例被創(chuàng)建后會通過attach方法傳入ContextWrapper舶掖。
接著,我又提出了疑問——為什么Toast不能直接在子線程中使用唾那?唯有看源碼來找答案了访锻。點進Toast的makeText方法可以看到:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context);
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;
}
沒發(fā)現(xiàn)什么特別的,不過我點進Toast的構(gòu)造方法看了看:
public Toast(Context context) {
mContext = context;
//TN是什么東西闹获?
mTN = new TN();
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);
}
發(fā)現(xiàn)一個我沒見過的TN類期犬,不過我很快就轉(zhuǎn)去show方法里看源碼:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
//又出現(xiàn)了TN這個類
TN tn = mTN;
tn.mNextView = mNextView;
try {
//注意這行
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
又出現(xiàn)了TN這個類,但正常情況下我們會先關(guān)注service.enqueueToast(pkg, tn, mDuration);這行代碼避诽。點進getService()看到:
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
看到Stub.asInterface龟虎,我們知道這是利用Binder進行跨進程調(diào)用了。百度上看到說INotificationManager service = getService()返回的就是NotificationManagerService沙庐,我也不知道怎么根據(jù)源碼來確定是NotificationManagerService類實現(xiàn)了enqueueToast()方法鲤妥,姑且就相信吧佳吞。功力有待加強。好慘好氣棉安。
而TN類就是遵循AIDL的實現(xiàn)底扳。接著我們看看TN類。TN類內(nèi)部使用Handler機制贡耽,post一個mShow和mHide:
private static class TN extends ITransientNotification.Stub {
//省略部分代碼
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
//下方有mShow的代碼
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
//mShow的代碼
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
}
快速瀏覽一下handleShow()方法的實現(xiàn)衷模。通過WindowManager的addView方法實現(xiàn)Toast的添加。其中trySendAccessibilityEvent()方法會把當(dāng)前的類名蒲赂、應(yīng)用的包名通過AccessibilityManager來做進一步的分發(fā)阱冶,以供后續(xù)的處理。代碼就不貼了滥嘴。
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
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.removeTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
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);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
看到這里木蹬,我們其實可以知道為了讓設(shè)備顯示出Toast,TN類的show()方法一定是在哪里被調(diào)起過若皱。我們關(guān)鍵要找一下到底哪里被調(diào)到镊叁。我Ctrl+F搜了一下Toast類,并沒有看到show()哪里被調(diào)用是尖,倒是發(fā)現(xiàn)hide()又被調(diào)用過意系。
于是乎我只好回過頭看看NotificationManagerService類里的enqueueToast()方法,發(fā)現(xiàn)答案就在這個方法里面!
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
//省略部分代碼
// (1) 判斷是否為系統(tǒng)Toast饺汹。如果當(dāng)前Toast所屬的進程的包名為“android”蛔添,則為系統(tǒng)Toast,
//否則還可以調(diào)用isCallerSystem()方法來判斷兜辞。
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
//如果它已經(jīng)在隊列中迎瞧,我們更新它,而不是將它放在隊列后頭
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
//限制toast的數(shù)量
//如果是非系統(tǒng)的Toast(通過應(yīng)用包名進行判斷)逸吵,且Toast的總數(shù)大于等于50凶硅,不再把新的Toast放入隊列。
//MAX_PACKAGE_NOTIFICATIONS的值為50
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
//通過keepProcessAliveLocked(callingPid)方法來設(shè)置對應(yīng)的進程為前臺進程扫皱,保證不被銷毀足绅。保證不會系統(tǒng)殺死。這也就解釋了為什么當(dāng)我們finish當(dāng)前Activity時韩脑,Toast還可以顯示氢妈,因為當(dāng)前進程還在執(zhí)行。
(4) index為0時段多,對隊列頭的Toast進行顯示首量。源碼如下:
keepProcessAliveLocked(callingPid);
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
//如果index = 0,說明Toast就處于隊列的頭部,直接進行顯示加缘。
if (index == 0) {
//注意這里鸭叙!點進去看看有沒有我們要的show()方法調(diào)用!
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
辛苦了各位拣宏,看到了這里沈贝,最后我們點進showNextToastLocked()方法看看:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//我在這我在這!看我看我J唇W撼獭!
record.callback.show();
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
終于找到show()方法被調(diào)用的地方啦市俊,差點我就放棄啦。好像網(wǎng)上并沒有很多人分析過Toast源碼滤奈,估計是司空見慣摆昧,或者說覺得太容易了吧,但我覺得多看點源碼還是好的蜒程。希望能因此提高自己的閱讀源碼能力绅你。
所以 ,為什么Toast不能直接在子線程中顯示昭躺,要用runOnUiThread處理一下忌锯?
Handler不能在子線程里運行的,因為子線程沒有創(chuàng)建Looper.prepare(); 所以就報錯了领炫。主線程不需要調(diào)用偶垮,是因為主線程已經(jīng)默認(rèn)幫你調(diào)用了。
除了runOnUiThread外帝洪,還有一種解決方案就是缺什么補什么——
new Thread(){
@Override
public void run() {
super.run();
Looper.prepare();
try {
Toast.makeText(MainActivity.this,title,Toast.LENGTH_SHORT).show();
}catch (Exception e) {
Logger.e("error",e.toString());
}
Looper.loop();
}
}.start();
最后似舵,用一張序列圖展示整個調(diào)用流程:
再文字講兩句:
如果當(dāng)前Toast所屬的進程的包名為“android”,則為系統(tǒng)Toast葱峡,否則還可以調(diào)用isCallerSystem()方法來砚哗;判斷當(dāng)前Toast所屬進程的uid是否為SYSTEM_UID、0砰奕、PHONE_UID中的一個蛛芥,如果是,則為系統(tǒng)Toast;如果不是军援,則不為系統(tǒng)Toast仅淑。 是否為系統(tǒng)Toast。系統(tǒng)Toast一定可以進入到系統(tǒng)Toast隊列中盖溺,不會被黑名單阻止漓糙。系統(tǒng)Toast在系統(tǒng)Toast隊列中沒有數(shù)量限制,而普通pkg所發(fā)送的Toast在系統(tǒng)Toast隊列中有數(shù)量限制烘嘱。
查看將要入隊的Toast是否已經(jīng)在系統(tǒng)Toast隊列中昆禽。這是通過比對pkg和callback來實現(xiàn)的蝗蛙。
將當(dāng)前Toast所在進程設(shè)置為前臺進程,這里的mAm=ActivityManagerNative.getDefault(),調(diào)用了setProcessForeground方法將當(dāng)前pid的進程置為前臺進程醉鳖,保證不會系統(tǒng)殺死捡硅。這也就解釋了為什么當(dāng)我們finish當(dāng)前Activity時,Toast還可以顯示盗棵,因為當(dāng)前進程還在執(zhí)行壮韭。