欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Toast 源码分析

程序员文章站 2024-03-05 11:54:42
...

思考

在分析源码之前有两个问题

  1. 子线程直接使用 Toast 时会抛出异常 “Can’t create handler inside thread that has not called Looper.prepare()”, 在 Toast 类里搜 “Looper” 并搜不到。是哪里抛出的这个异常呢?

  2. 在子线程中这样使用 Toast:

Looper.prepare();
Toast.makeText(getApplicationContext(), "test toast", Toast.LENGTH_SHORT).show();
Looper.loop();

像上面这种调用是在主线程显示 Toast 吗

Toast 源码分析

先看 Toast 的 makeText 和 show 方法:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    // 创建 TN(Toast 的内置对象),用于展示 Toast 的主要类
    Toast result = new Toast(context);

    // 加载 Toast 的展示内容
    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;
}

public void show() {
    // 校验显示内容
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService(); // NotificationManagerService 的成员变量 mService 是 INotificationManager.Stub 的实现类
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    // 展示内容属性赋值给 tn 的属性
    tn.mNextView = mNextView;

    try {
        // 调用 NotificationManagerService 中 mService 的 enqueueToast 方法
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

下面来看 NotificationManagerService 中 mService 的 enqueueToast 方法:

// callback 就是 TN 实例
@Override
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 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 {
                // ...

                Binder token = new Binder();
                mWindowManagerInternal.addWindowToken(token,
                        WindowManager.LayoutParams.TYPE_TOAST);
                // 创建 ToastRecord 对象
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                // mToastQueue 添加记录
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveIfNeededLocked(callingPid);
            }
            if (index == 0) {
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

上面代码可以看到有往 mToastQueue 中添加记录,那么什么时候执行呢,全局搜 “mToastQueue.get(” 看到在 showNextToastLocked 方法中取出 mToastQueue 的第 0 个元素,调用 record.callback.show(record.token), record.callback 就是 TN 对象,也就是这里调了 TN 的 show 方法。下面就来看一下 TN 的 show 方法做了什么:

public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    // 交给 mHandler 处理
    mHandler.obtainMessage(0, windowToken).sendToTarget();
}

// TN 的 mHandler
final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // 所以 TN 的 show 方法处理的是这里
        IBinder token = (IBinder) msg.obj;
        handleShow(token);
    }
};

下面来看核心展示 Toast 的方法,也就是 TN 的 handleShow() 方法

public void handleShow(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
            + " mNextView=" + mNextView);
    if (mView != mNextView) {
        handleHide();
        mView = mNextView;
        // ...
        // 展示使用的是 WindowManager
        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

        // 下面配置的是位置参数
        final Configuration config = mView.getContext().getResources().getConfiguration();
        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;
        mParams.hideTimeoutMilliseconds = mDuration ==
            Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
        mParams.token = windowToken;
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeView(mView);
        }
        if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
        // 展示 Toast, 由此也可以看出,Toast 是被添加在 Window 下的
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    }
}

总结

Toast 是先调用 makeText 创建要展示的 view, 调用 show 方法时内部跨进程通知 NotificationManagerService, 再通知到 TN 执行 show 方法,最终通过 handler 添加到 Window 下。
再看下最开始的思考,

  1. makeText 会 new Toast, Toast 构造方法里创建 TN 对象,TN 有一个成员变量 mHandler, 在创建 Handler 时需要该线程有 Looper 对象,主线程的 ActivityThread 的 main() 方法有创建 Looper 对象,但子线程并没有 Looper 对象,所以会抛异常。

  2. 如果在子线程调 Looper.prepare(), 会是在主线程展示的 Toast 吗,看上面的代码很明显不是,因为 Toast, TN 都是在子线程创建的,所以 mHandler 也是在子线程创建的,处理消息时的 Looper 也是子线程创建的,所以展示也是在子线程的。

相关标签: Toast