当前位置: 首页 > news >正文

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();
    }
}
复制代码

相关文章:

  • AutoCAD 命令统计魔幻球的实现过程--(3)
  • SeimiCrawler 2.0版本变动介绍
  • DNS服务的配置与管理(5) 配置转发器
  • 基于注解实现SpringBoot多数据源配置
  • shell if 参数
  • 换个角度看问题
  • Lr(3)-脚本调试之“参数化、检查点”
  • 添加删除mysql用户
  • dp学习笔记1
  • AT&T以11亿美元的价格将数据中心出售给Brookfield
  • mysql开启常规日志
  • js里的数据转换
  • sql删除重复数据只保留一条
  • 构建可观测的分布式系统
  • centos 普通用户获得sudo超级权限
  • bearychat的java client
  • Flannel解读
  • rabbitmq延迟消息示例
  • Vue 动态创建 component
  • vue-router的history模式发布配置
  • Yii源码解读-服务定位器(Service Locator)
  • 复杂数据处理
  • 官方新出的 Kotlin 扩展库 KTX,到底帮你干了什么?
  • 机器学习学习笔记一
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 技术胖1-4季视频复习— (看视频笔记)
  • 解决iview多表头动态更改列元素发生的错误
  • 开发基于以太坊智能合约的DApp
  • 面试总结JavaScript篇
  • 实战|智能家居行业移动应用性能分析
  • scrapy中间件源码分析及常用中间件大全
  • 带你开发类似Pokemon Go的AR游戏
  • ​​​​​​​Installing ROS on the Raspberry Pi
  • ​Z时代时尚SUV新宠:起亚赛图斯值不值得年轻人买?
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #define、const、typedef的差别
  • (1)STL算法之遍历容器
  • (16)Reactor的测试——响应式Spring的道法术器
  • (2/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (env: Windows,mp,1.06.2308310; lib: 3.2.4) uniapp微信小程序
  • (Python) SOAP Web Service (HTTP POST)
  • (笔记)Kotlin——Android封装ViewBinding之二 优化
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (附源码)小程序 交通违法举报系统 毕业设计 242045
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • .bat批处理(四):路径相关%cd%和%~dp0的区别
  • .Net Core与存储过程(一)
  • .net framwork4.6操作MySQL报错Character set ‘utf8mb3‘ is not supported 解决方法
  • .NET开源项目介绍及资源推荐:数据持久层 (微软MVP写作)
  • @RequestMapping处理请求异常
  • [ 攻防演练演示篇 ] 利用通达OA 文件上传漏洞上传webshell获取主机权限
  • [Android]How to use FFmpeg to decode Android f...
  • [C/C++]_[初级]_[关于编译时出现有符号-无符号不匹配的警告-sizeof使用注意事项]