Toast 特性
- Toast继承子Object,不是View。
- Toast 弹出后当前 Activity 仍然保持可见性和可交互性。
- 不会获取到焦点。
- 超时后会自动消失。
- 可以自定义显示在屏幕上的位置。
- 可以使用自定义布局,也只有在自定义布局的时候才需要直接调用 Toast 的构造方法,其它时候都是使用 makeText 方法来创建 Toast。
- 使用 cancel 方法可以立即将已显示的 Toast 关闭,让未显示的 Toast 不再显示。
- Toast 也算是一个「通知」,如果弹出状态消息后期望得到用户响应,应该使用 Notification。
- 应用在后台时可以调用 Toast 并正常弹出。
- Toast 队列里允许单个应用往里添加 50 个 Toast,超出的将被丢弃。
Toast 使用
基本使用
Context context = getApplicationContext();
CharSequence text = "Hello toast!";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
复制代码
或者
Toast.makeText(getApplicationContext(), "Hello toast!", Toast.LENGTH_SHORT).show();
复制代码
调整 Toast 位置
toast.setGravity(Gravity.TOP|Gravity.LEFT, 0, 0);
复制代码
自定义 Toast
自定义布局 layout/custom_toast.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/custom_toast_container"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="8dp"
android:background="#DAAA"
>
<ImageView android:src="@drawable/droid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
/>
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFF"
/>
</LinearLayout>
复制代码
加载布局,并设置信息:
LayoutInflater inflater = getLayoutInflater();
View layout = inflater.inflate(R.layout.custom_toast,
(ViewGroup) findViewById(R.id.custom_toast_container));
TextView text = (TextView) layout.findViewById(R.id.text);
text.setText("This is a custom toast");
Toast toast = new Toast(getApplicationContext());
toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
toast.setDuration(Toast.LENGTH_LONG);
toast.setView(layout);
toast.show();
复制代码
Toast 显示流程
/** @hide */
@IntDef({LENGTH_SHORT, LENGTH_LONG})
@Retention(RetentionPolicy.SOURCE)
public @interface Duration {}
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;
}
复制代码
duration参数用注解指定了只能是 LENGTH_SHORT, LENGTH_LONG类型。该方法主要构造了Toast对象,加载Toast显示的布局,并设置显示的文本,然后给一些Toast实例的变量赋了值。 com.android.internal.R.layout.transient_notification 这个布局的内容:
<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_marginHorizontal="24dp"
android:layout_marginVertical="15dp"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/primary_text_default_material_light"
/>
</LinearLayout>
复制代码
Toast类的构造函数:
public Toast(Context context) {
mContext = context;
mTN = new TN(context.getPackageName(), looper); // looper 为 null
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);
}
复制代码
Toast的构造函数中创建了TN实例,是Toast定义的内部类,此时的looper实例为null,所以如果在子线程中没有调用Looper.prepare();和Looper.loop();直接使用Toast的show()方法将会抛出异常。 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
。 然后设置了方位和y轴的偏移量。
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
TN(String packageName, @Nullable Looper looper) {
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;
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();
...
break;
}
...
}
}
};
}
@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
@Override
public void hide() {
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
....
mParams.token = windowToken;
...
mWM.addView(mView, mParams);
...
}
}
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mView = null;
}
}
}
复制代码
TN继承自ITransientNotification.Stub,是一个Binder类。可见这里是通过Binder进行远程调用的。ITransientNotification.aidl:
/** @hide */
oneway interface ITransientNotification {
void show(IBinder windowToken);
void hide();
}
复制代码
到这边之后Toast实例就构造完毕了。显示的时候就调用实例的show()方法:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();//调用系统的notification服务
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
private static INotificationManager sService;
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
复制代码
获取Notification Service实例的远程代理,它处理系统范围内的正确排序。当 Toast 在 show 的时候,将这个请求放在 NotificationManager 所管理的队列中,并且为了保证 NotificationManager 能跟进程交互, 会传递一个 TN 类型的 Binder 对象给 NotificationManager 系统服务。而在 NotificationManager 系统服务中:
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
...
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
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;
}
}
}
}
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);
}
//如果当前没有toast,显示当前
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
复制代码
假设系统中只有一个Toast需要显示,则调用showNextToastLocked()方法:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
record.callback.show(record.token);//通知进程显示
scheduleTimeoutLocked(record);//超时监听消息
return;
} catch (RemoteException e) {
...
}
}
}
复制代码
showNextToastLocked() 函数将调用 ToastRecord的 callback 成员的 show 方法通知进程显示, callback就是传入参数的TN的Binder代理对象,show()方法会调用到handleShow()方法:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
....
mParams.token = windowToken;
...
mWM.addView(mView, mParams);
...
}
}
复制代码
调用 WindowManager.addView 方法,将 Toast 中的 mView 对象纳入 WMS 的管理。showNextToastLocked() 函数,而这个方法就是用于管理 Toast 时序:
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);
}
复制代码
scheduleTimeoutLocked 内部通过调用 Handler 的 sendMessageDelayed 函数来实现定时调用,而这个 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) {
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
复制代码
WorkerHandler 处理 MESSAGE_TIMEOUT 消息会调用 handleTimeout(ToastRecord) 函数,而 handleTimeout(ToastRecord) 函数经过搜索后,将调用 cancelToastLocked 函数取消掉 Toast 的显示:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
...
}
mToastQueue.remove(index);
if (mToastQueue.size() > 0) {
...
showNextToastLocked();
}
}
复制代码
函数将调用 ToastRecord的 callback 成员的 hide 方法通知进程显示, callback就是传入参数的TN的Binder代理对象,hide()方法会调用到handleHide()方法:
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mView = null;
}
}
复制代码
调用 WindowManager.removeView 方法,将 Toast 中的 mView 对象排除出 WMS 的管理。
有时候Android 进程某个 UI 线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法之前。这就导致了android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@aadb5ab is not valid; is your activity running?
或者由于某些系统增加下面实现:
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
复制代码
导致不会抱错,但是也不会显示Toast。
防止 Toast 被移除后错误显示
我们知道,Toast 的窗口属于系统窗口,它的生成和生命周期依赖于系统服务 NotificationManager。一旦 NotificationManager 所管理的窗口生命周期跟我们本地的进程不一致,就会发生异常。
所以我们可以选择自己可以控制的窗口类型:
- 使用子窗口: 在 Android 进程内,我们可以直接使用类型为子窗口类型的窗口。在 Android 代码中的直接应用是 PopupWindow 或者是 Dialog 。这当然可以,不过这种窗口依赖于它的宿主窗口,它可用的条件是你的宿主窗口可用。
- 采用 View 系统: 使用 View 系统去模拟一个 Toast 窗口行为,做起来不仅方便,而且能更加快速的实现动画效果,在 Android 代码中的直接应用是 SnackBar 。
往哪个控件中添加 Toast 控件?
在Android进程中,我们所有的可视操作都依赖于一个 Activity 。 Activity 提供上下文(Context)和视图窗口(Window) 对象。我们通过 Activity.setContentView 方法所传递的任何 View对象 都将被视图窗口( Window) 中的 DecorView 所装饰。而在 DecorView 的子节点中,有一个 id 为 android.R.id.content 的 FrameLayout 节点(后面简称 content 节点) 是用来容纳我们所传递进去的 View 对象。一般情况下,这个节点占据了除了通知栏的所有区域。这就特别适合用来作为 Toast 的父控件节点。
什么时候往这个content节点中添加 Toast 控件?
content 节点生成
content 节点包含在我们的 DecorView 控件中,而 DecorView 是由 Activity 的 Window对象所持有的控件。Window 在 Android 中的实现类是 PhoneWindow:
//code PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) { //mContentParent就是我们的 content 节点
installDecor();//生成一个DecorView
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
复制代码
PhoneWindow 对象通过 installDecor 函数生成 DecorView 和 我们所需要的 content 节点(最终会存到 mContentParent) 变量中去。但是, setContentView 函数需要我们主动调用,如果我并没有调用这个 setContentView 函数,installDecor 方法将不被调用。除了在 setContentView 函数中调用installDecor外,还有一个函数也调用到了这个,那就是:
//code PhoneWindow.java
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
复制代码
而这个函数,将在 Activity.findViewById 的时候调用:
//code Activity.java
public View findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
//code Window.java
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
复制代码
因此,只要我们只要调用了 findViewById 函数,一样可以保证 content 被正常初始化。
content 内容显示
Android 界面什么时候显示? 实际上,在 onResume 的时候,根本还没处理跟界面相关的事情。我们来看下 Android 进程是如何处理 resume 消息的: (注: AcitivityThread 是 Android 进程的入口类, Android 进程处理 resume 相关消息将会调用到 AcitivityThread.handleResumeActivity 函数):
//code AcitivityThread.java
void handleResumeActivity(...) {
...
ActivityClientRecord r = performResumeActivity(token, clearHide);
// 之后会调用call onResume
...
View decor = r.window.getDecorView();
//调用getDecorView 生成 content节点
decor.setVisibility(View.INVISIBLE);
....
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();//add to WM 管理
}
...
}
//code Activity.java
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
复制代码
Android 进程在处理 resume 消息的时候,将走以下的流程:
调用 performResumeActivity 回调 Activity 的 onResume 函数 调用 Window 的 getDecorView 生成 DecorView 对象和 content 节点 将DecorView纳入 WindowManager (进程内服务)的管理 调用 Activity.makeVisible 显示当前 Activity 按照上述的流程,在 Activity.onResume 回调之后,才将控件纳入本地服务 WindowManager 的管理中。也就是说, Activity.onResume 根本没有显示任何东西。
Android的绘制是什么时候开始的?又是到什么时候结束?
在 Android 系统中,每一次的绘制都是通过一个 16ms 左右的 VSYNC 信号控制的,这种信号可能来自于硬件也可能来自于软件模拟。每一次非动画的绘制,都包含:测量,布局,绘制三个函数。而一般触发这一事件的的动作有:
- View 的某些属性的变更
- View 重新布局Layout
- 增删 View 节点 当调用 WindowManager.addView 将空间添加到 WM 服务管理的时候,会调用一次Layout请求,这就触发了一次 VSYNC 绘制。因此,我们只需要在 onResume 里 post 一个帧回调就可以检测绘制开始的时间:
@Override
protected void onResume() {
super.onResume();
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//TODO 绘制开始
}
});
}
复制代码
View.requestLayout 是怎么触发界面重新绘制的:
//code View.java
public void requestLayout() {
....
if (mParent != null) {
...
if (!mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
}
复制代码
View 对象调用 requestLayout 的时候会委托给自己的父节点处理,这里之所以不称为父控件而是父节点,是因为除了控件外,还有 ViewRootImpl 这个非控件类型作为父节点,而这个父节点会作为整个控件树的根节点。按照我们上面说的委托的机制,requestLayout 最终将会调用到 ViewRootImpl.requestLayout。
//code ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();//申请绘制请求
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
....
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//申请绘制
....
}
}
复制代码
ViewRootImpl 最终会将 mTraversalRunnable 处理命令放到 CALLBACK_TRAVERSAL 绘制队列中去:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();//执行布局和绘制
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
...
performTraversals();
...
}
}
复制代码
mTraversalRunnable 命令最终会调用到 performTraversals() 函数:
private void performTraversals() {
final View host = mView;
...
host.dispatchAttachedToWindow(mAttachInfo, 0);//attachWindow
...
getRunQueue().executeActions(attachInfo.mHandler);//执行某个指令
...
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);//测量
....
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//布局
...
draw(fullRedrawNeeded);//绘制
...
}
复制代码
performTraversals 函数实现了以下流程:
- 调用 dispatchAttachedToWindow 通知子控件树当前控件被 attach 到窗口中
- 执行一个命令队列 getRunQueue
- 执行 meausre 测量指令
- 执行 layout 布局函数
- 执行绘制 draw 这里我们看到一句方法调用:
getRunQueue().executeActions(attachInfo.mHandler);
复制代码
这个函数将执行一个延时的命令队列,在 View 对象被 attach 到 View树之前,通过调用 View.post 函数,可以将执行消息命令加入到延时执行队列中去:
//code View.java
public boolean post(Runnable action) {
Handler handler;
AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
handler = attachInfo.mHandler;
} else {
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
return handler.post(action);
}
复制代码
getRunQueue().executeActions 函数执行的时候,会将该命令消息延后一个UI线程消息执行,这就保证了执行的这个命令消息发生在我们的绘制之后:
//code RunQueue.java
void executeActions(Handler handler) {
synchronized (mActions) {
...
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);//推迟一个消息
}
}
}
复制代码
所以,我们只需要在视图被 attach 之前通过一个 View 来抛出一个命令消息,就可以检测视图绘制结束的时间点:
//code DemoActivity.java
@Override
protected void onResume() {
super.onResume();
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
start = SystemClock.uptimeMillis();
log("绘制开始:height = "+view.getHeight());
}
});
}
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new View(this);
view.post(new Runnable() {
@Override
public void run() {
log("绘制耗时:"+(SystemClock.uptimeMillis()-start)+"ms");
log("绘制结束后:height = "+view.getHeight());
}
});
this.setContentView(view);
}
//控制台输出:
01-03 23:39:27.251 27069 27069 D cdw : --->绘制开始:height = 0
01-03 23:39:27.295 27069 27069 D cdw : --->绘制耗时:44ms
01-03 23:39:27.295 27069 27069 D cdw : --->绘制结束后:height = 1232
复制代码
基于Toast的改法
Toast.show 实际上只是发了一条命令给 NotificationManager 服务。真正的显示需要等 NotificationManager 通知我们的 TN 对象 show 的时候才能触发。NotificationManager 通知给 TN 对象的消息,都会被 TN.mHandler 这个内部对象进行处理:
/code Toast.java
private static class TN {
final Runnable mHide = new Runnable() {// 通过 mHandler.post(mHide) 执行
@Override
public void run() {
handleHide();
mNextView = null;
}
};
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);// 处理 show 消息
}
};
}
复制代码
在NotificationManager 通知给 TN 对象显示的时候,TN 对象将给 mHandler 对象发送一条消息,并在 mHandler 的 handleMessage 函数中执行。 当NotificationManager 通知 TN 对象隐藏的时候,将通过 mHandler.post(mHide) 方法,发送隐藏指令。不论采用哪种方式发送的指令,都将执行 Handler 的 dispatchMessage(Message msg) 函数:
//code Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);// 执行 post(Runnable)形式的消息
} else {
...
handleMessage(msg);// 执行 sendMessage形式的消息
}
}
复制代码
因此,我们只需要在 dispatchMessage 方法体内加入 try-catch 就可以避免 Toast 崩溃对应用程序的影响:
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch(Exception e) {}
}
复制代码
因此,我们可以定义一个安全的 Handler 装饰器:
private static class SafelyHandlerWarpper extends Handler {
private Handler impl;
public SafelyHandlerWarpper(Handler impl) {
this.impl = impl;
}
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (Exception e) {}
}
@Override
public void handleMessage(Message msg) {
impl.handleMessage(msg);//需要委托给原Handler执行
}
}
复制代码
由于 TN.mHandler 对象复写了 handleMessage 方法,因此,在 Handler 装饰器里,需要将 handleMessage 方法委托给 TN.mHandler 执行。定义完装饰器之后,我们就可以通过反射往我们的 Toast 对象中注入了:
public class ToastUtils {
private static Field sField_TN ;
private static Field sField_TN_Handler ;
static {
try {
sField_TN = Toast.class.getDeclaredField("mTN");
sField_TN.setAccessible(true);
sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
sField_TN_Handler.setAccessible(true);
} catch (Exception e) {}
}
private static void hook(Toast toast) {
try {
Object tn = sField_TN.get(toast);
Handler preHandler = (Handler)sField_TN_Handler.get(tn);
sField_TN_Handler.set(tn,new SafelyHandlerWarpper(preHandler));
} catch (Exception e) {}
}
public static void showToast(Context context,CharSequence cs, int length) {
Toast toast = Toast.makeText(context,cs,length);
hook(toast);
toast.show();
}
}
复制代码