Toast 学习

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();
    }
}
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值