Toast是一种提供给用户简洁信息的视图,该视图已浮于应用程序之上的形式呈现给用户。 Toast源码位于:frameworks\base\core\java\android\widget\Toast.java
Toast使用就一行代码:
Toast.makeText(ToastActivity.this,"Toast源码解析",Toast.LENGTH_LONG).show();
Toast 提供了setView方法,可以自定义View:
Toast toast=new Toast(ToastActivity.this);
View view=View.inflate(ToastActivity.this,R.layout.toast_view,null);
toast.setView(view);
toast.setGravity(Gravity.CENTER,0,0);
toast.show();
toast_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
</LinearLayout>
Toast源码分析有两个目标,知道Toast源码在哪里体现了Toast显示,又在哪里体现了Toast消失。首先从Toast的基本使用,作为入口。
Toast.makeText(ToastActivity.this,"Toast源码解析",Toast.LENGTH_LONG).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);
//传入下个view
result.mNextView = v;
//Toast显示的时间长度
result.mDuration = duration;
return result;
}
对应的transient_notification.xml,源码位于:frameworks\base\core\res\layout\transient_notification.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/bright_foreground_dark"
android:shadowColor="#BB000000"
android:shadowRadius="2.75"
/>
</LinearLayout>
从makeText方法看,就是Toast的自定义view的那部分代码。
再来看看show方法:
public void show() {
//解释自定义view,需要先setView
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//得到INotificationManager服务
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
//TN对象
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
看了show方法,发现涉及两个新的类,TN 和INotificationManager 。enqueueToast方法大概就是实现Toast显示和消失吧,让我们一步步探索。
TN类继承自ITransientNotification.Stub,ITransientNotification.aidl,用于进程间通信,源码位于frameworks\base\core\java\android\app\ITransientNotification.aidl
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();
//出现在屏幕的位置
int mGravity;
//分别是出现在屏幕的X、Y方向偏移量
int mX, mY;
//横向margin值
float mHorizontalMargin;
//竖向margin值
float mVerticalMargin;
//当前view
View mView;
//下个Toast显示的view
View mNextView;
WindowManager mWM;
//TN构造函数
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");
// 设置flag
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
*/
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
//判断下个view是否一样
if (mView != mNextView) {
// remove the old view if necessary
//移除当前view
handleHide();
mView = mNextView;
//获取当前view上下文
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
//获得 WindowManager对象
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();
//获取绝对的Gravity
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;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
//如果当前view存在,先移除
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
//通过WindowManager调用addView加载
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
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);
}
/**
* WindowManager调用removeView方法来将Toast视图移除
*/
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.removeView(mView);
}
mView = null;
}
}
}
来看下ITransientNotification ,就两个方法,如下:
/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
具体实现就在TN类,其他进程回调TN类,来操作Toast的显示和消失:
@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显示和消失用的Handler机制实现的。
调用了getService,如下:
private static INotificationManager sService;
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
得到INotificationManager服务,再调用enqueueToast方法,参数有三个,包名,TN,时间。INofiticationManager接口的具体实现类是NotificationManagerService类,源码位置:frameworks\base\services\core\java\com\android\server\notification\NotificationManagerService.java
加入队列,用来显示Toast,队列最大数50
@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 ;
}
//(1)判断是否系统的Toast,如果当前包名是android则为系统
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
//判断当前toast所属的pkg是不是所阻止的
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
}
//入队列mToastQueue
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
//(2)判断Toast是否在队列当中
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.
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.
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++;
//toasts最大数50个
if (count >= 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中
mToastQueue.add(record);
index = mToastQueue.size() - 1;
//(3)设置该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.
if (index == 0) {
//(4)直接显示Toast
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
(1)判断是否系统的Toast,源码:
private static boolean isCallerSystem() {
return isUidSystem(Binder.getCallingUid());
}
private static boolean isUidSystem(int uid) {
final int appid = UserHandle.getAppId(uid);
// 判断pid为系统进程使用的用户id,值为1000,或者为系统进程的手机的用户id,值为1001
return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
}
(2)判断Toast是否在队列当中,源码:
// lock on mToastQueue
int indexOfToastLocked(String pkg, ITransientNotification callback)
{
//
IBinder cbak = callback.asBinder();
ArrayList<ToastRecord> list = mToastQueue;
int len = list.size();
for (int i=0; i<len; i++) {
ToastRecord r = list.get(i);
if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
return i;
}
}
return -1;
}
(3)设置该Toast为前台进程,源码:
// lock on mToastQueue
void keepProcessAliveLocked(int pid)
{
// toasts from this pid
int toastCount = 0;
ArrayList<ToastRecord> list = mToastQueue;
int N = list.size();
for (int i=0; i<N; i++) {
ToastRecord r = list.get(i);
if (r.pid == pid) {
toastCount++;
}
}
try {
// 设置该Toast为前台进程
mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0);
} catch (RemoteException e) {
// Shouldn't happen.
}
}
(4)直接显示Toast,源码:
void showNextToastLocked() {
//直接取第一个
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//回调TN类,显示Toast
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;
}
}
}
}
private void scheduleTimeoutLocked(ToastRecord r)
{
//移除ToastRecord
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
//static final int LONG_DELAY = 3500; // 3.5 seconds
//static final int SHORT_DELAY = 2000; // 2 seconds
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
//发送Toast消失的message
mHandler.sendMessageDelayed(m, delay);
}
从enqueueToast方法可知,先判断是不是系统和合法的Toast,然后判断是否在ToastQueue(这里解释了很多Toast,是一个个显示的),如果存在,只需要更新Toast显示的时间,如果不在,就直接显示,回调给TN类。到这里,知道了Toast是如何显示的。
还没有结束,继续追踪mHandler,来到WorkerHandler :
private final class WorkerHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
//……
}
}
}
private void handleTimeout(ToastRecord record)
{
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
//还是判断Toast是否在队列当中
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
//回调TN类,Toast消失
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
// don't worry about this, we're about to remove it from
// the list anyway
}
//该ToastRecord对象从mToastQueue中移除
mToastQueue.remove(index);
//设置该Toast为前台进程
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
//继续show下个Toast
showNextToastLocked();
}
}
到这里,知道了Toast是如何消失的。Toast核心代码显示和消失源码分析完毕。
Toast代码调用只有一行,了解这行代码的背后,有个ToastQueue,进入队列之前,会做一些合法性判断,使用进程间通信进行回调,Handler机制显示和消失。自定义Toast时,需要调用setView,不然show会抛异常,这个从show方法就能得知。至此,Toast源码解析告一段落。