編輯:關於Android編程
Toast是一種向用戶快速提供少量信息的視圖。當它顯示時,它會浮在整個應用層的上面,並且不會獲取到焦點。它的設計思想是能夠向用戶展示些信息,但又能盡量不顯得唐突。本篇我們來研讀一下Toast的源碼,並探明它的顯示及隱藏機制。
我們從Toast的最簡單調用開始,它的調用代碼是:
Toast.makeText(context,"Show toast",Toast.LENGTH_LONG).show();
在上面的代碼中,我們是先調用Toast
的靜態方法來創建一個Toast
,然後調用其show()
方法將其顯示出來。其中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;
}
可以看到只是new出一個Toast,並設置要顯示的View及時間。這裡默認使用的Toast布局transient_notification.xml
也在SDK,代碼如下:
它只是一個簡單的LinearLayout套一個TextView。順便說一句,從布局上我們可以知道這個Toast的背景顏色是可以配置的,通過在theme中配置toastFrameBackground
屬性。
回到Toast,我們來看一下它的屬性定義及構造方法。
public class Toast {
final Context mContext;
final TN mTN;
int mDuration;
View mNextView;
public Toast(Context context) {
mContext = context;
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);
}
它的屬性很簡單,只有四個,分別是上下文對象mContext
,TN
對象mTN
,表示顯示時長的mDuration
,以及表示將要顯示的視圖mNextView
。在它的構造方法中,初始化mTN
及它的兩個成員變量。
留意一下Toast中顯示的View的命名為mNextView
。
我們再往下翻一下Toast的一些成員方法,會發現它的許多行為的實現,都是把值賦給了mTN所對應的屬性,比如設置gravity
,水平外邊距,x軸或y軸的偏移等等。而它的show
及cancel
方法,也都是通過其來實現,如下:
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
}
}
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
那麼TN究竟是個什麼東西?
我們來看一下TN的源碼,可以知道,它是Toast裡的一個靜態內部類,繼承自ITransientNotification.Stub
,並實現了其顯示和隱藏的方法:
private static class TN extends ITransientNotification.Stub {
// 顯示命令
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;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();//懸浮窗口的參數
final Handler mHandler = new Handler(); //用於將操作命令添加到線程的隊列中
//顯示的View的相關布局參數
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView; //當前顯示的View
View mNextView;// 下一個要顯示的View
WindowManager mWM;//用於顯示懸浮窗口
TN() {
// 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;
}
/**
* 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);//將隱藏命令添加到線程隊列當中
}
// ...其他代碼
}
從上面的代碼中,我們可以看到,一個Toast的顯示和隱藏,並不是在Toast類本身中實現的,而是交給了TN,由它去實現。TN則是包裝了一個Toast的內容及行為。TN的顯示及隱藏的具體實現handleShow()
和handleHide()
,就是向WindowManager添加和隱藏View,就和我們平時寫懸浮窗口的實現一樣,這裡略過。
我們還可以看到,TN繼承的ITransientNotification.Stub
,它是AIDL接口的實現,該接口為ITransientNotification.aidl
。AIDL是Android中用於進程間通信的一種機制。也就是對於我們的Toast,最終是交給另一個進程去顯示及隱藏的。
我們回到剛才讀Toast時的show()
及cancel()
的代碼,可以看到先是獲取一個服務INotificationManager service = getService();
,顯示時調用其service.enqueueToast(pkg, tn, mDuration);
來顯示,隱藏時調用mTN
的hide()
方法,並接著調用getService().cancelToast(mContext.getPackageName(), mTN);
來取消,其中,mTN
是它的進程通信的回調。
##INotificationManager
INotificationManager
也是一個AIDL接口,它定義一些通知管理服務的API,位於Android源碼中的frameworks\base\core\java\android\app
,並沒有包含在Android SDK中。代碼如下:
interface INotificationManager
{
/** @deprecated use {@link #enqueueNotificationWithTag} instead */
void enqueueNotification(String pkg, int id, in Notification notification, inout int[] idReceived);
/** @deprecated use {@link #cancelNotificationWithTag} instead */
void cancelNotification(String pkg, int id);
void cancelAllNotifications(String pkg);
void enqueueToast(String pkg, ITransientNotification callback, int duration);
void cancelToast(String pkg, ITransientNotification callback);
void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived);
void cancelNotificationWithTag(String pkg, String tag, int id);
}
可以看到它管理的內容包含Toast提示及通知,下面我們只關注Toast相關的兩個接口。根據它的名字,我們找到這個服務的對應實現sources\android-23\android\app\NotificationManager.java
。
在該類的代碼中,有INotificationManager.Stub
的匿名內部類實例,主要代碼如下:
private final IBinder mService = new INotificationManager.Stub() {
// Toasts
// ============================================================================
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) {
Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
+ " duration=" + duration);
}
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
}
//對Toast隊列加鎖
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();//獲取調用進程id
long callingId = Binder.clearCallingIdentity();//重置當前線程上進來的IPC的ID
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);//判斷我們的mTN是否存在隊列中
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
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,則限制toast的數量,以避免DOS攻擊及內存洩露。
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
// 創建一個ToastRecord對象,並加入隊列。
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);//設置調用者的進程的活動狀態。它會根據該進程的Toast的數量來設置是否為前台進程。
}
// 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.
//如果最後一個下標為0,則表示它是當前的toast,那麼將不會管它是新創建的還是剛剛被更新過,
//而是回調告訴mTN把自己顯示出來。
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
@Override
public void cancelToast(String pkg, ITransientNotification callback) {
Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
return ;
}
synchronized (mToastQueue) {
long callingId = Binder.clearCallingIdentity();
try {
//獲取Toast的在隊列中的下標
int index = indexOfToastLocked(pkg, callback);
if (index >= 0) {//如果存在
//從隊列中移除,該方法還會設置調用進程的活動狀態,並顯示下一條要顯示的Toast
cancelToastLocked(index);
} else {
Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
+ " callback=" + callback);
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
//其他接口的實現代碼略
}
其中顯示toast的代碼如下:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
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;
}
}
}
}
````
可以看到它會調用`TN`對象的`show()`方法,並接著調用`scheduleTimeoutLocked(record);`。
該方法代碼如下:
```java
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
也就是在delay時間之後發送一個MESSAGE_TIMEOUT的消息來通知Toast隱藏。delay的時間根據Toast設置的duration是否為Toast.LENGTH_LONG來決定是LONG_DELAY(3.5秒)還是SHORT_DELAY(2秒)
NotificationManagerService
的實現代碼很長,為避免篇幅因貼上代碼的()() 關系變得太長,這裡只介紹了主要流程的代碼,具體每個步驟的方法實現,可以自己翻閱該類代碼,文件位於SDK的
sources\android-23\com\android\server\notification1`。
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
也就是在delay時間之後發送一個MESSAGE_TIMEOUT的消息來通知Toast隱藏。delay的時間根據Toast設置的duration是否為Toast.LENGTH_LONG來決定是LONG_DELAY(3.5秒)還是SHORT_DELAY(2秒)。
NotificationManagerService
的實現代碼很長,為避免篇幅因貼上代碼的()() 關系變得太長,這裡只介紹了主要流程的代碼,具體每個步驟的方法實現,可以自己翻閱該類代碼,文件位於SDK的
sources\android-23\com\android\server\notification1`。
對於Toast代碼的分析在此告一段落,從上面的分析中我們可以得到以下結論:
默認的Toast布局的背景可以在theme中配置。 Toast的顯示及隱藏是通過NotificationManagerService來管理的,它跨進程,使用AIDL來實現進程間通信。 所有Toast都會加到NotificationManagerService
的隊列中,對於非系統程序,它會限制Toast的數量(當前我所讀的代碼中該值為50)以防止DOS攻擊及內存洩露的問題。 Toast的顯示及隱藏命令通過new出來的handler來發送。所以沒有隊列的線程是不能顯示Toast的。 Toast的顯示的時間只有兩個,duration相當於一個標志位,用於標志顯示的時間是長還是短,而不是具體的顯示時間。 當有Toast要顯示時,其所在進程會被設為前台進程。
服務,作為Android四大組件之一,必然是重點。我們今天就來講解一下有關服務的生命周期、兩種開啟方式以及相關用法。 服務有兩種開啟方式,一種是正常開啟, 一種是以綁定的
0x00我們首先講一個webView這個方法的作用:webView.getSettings().setAllowFileAccessFromFileURLs(false)
寫在前面:最近接到老大的一個需求,要求在手機端攔截微信的通知(Notification),從而獲得聯系人和內容。之後將聯系人和內容發送到我們的硬件產品上,展示出來之後,再
上次我們實現了利用viewpager實現對新用戶的功能性介紹,今天我們來顯示利用浮動窗口對用戶進行操作的引導。先看效果圖。 雖然界面比較丑,但是可以看到我們還是可以實現對