美文网首页
Toast几个面试题与代码导读

Toast几个面试题与代码导读

作者: C调路过 | 来源:发表于2020-03-15 00:40 被阅读0次

能否在子线程Toast

第一种情况,直接new一个线程进行Toast

            new Thread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(ToastActivity.this, "在子线程Toast", Toast.LENGTH_LONG).show();
                }
            }).start();

结果报错崩溃

java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()

原因为Toast需要使用Handler事件分发,必须保证运行在looper中。
然后在子线程创建了Handler,确实可以弹出┓( ´∀` )┏。

    new Thread(new Runnable() {
        @Override
        public void run() {

            Looper.prepare();

            handler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    Toast.makeText(ToastActivity.this, "在子线程Toast", Toast.LENGTH_LONG).show();
                }
            };

            Looper.loop();
        }
    }).start();

    findViewById(R.id.btn_toast_sub).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            handler.sendEmptyMessage(0);

        }
    });

也可以使用HandlerThread来初始化Looper,HandlerThread的使用参考IntentService。

    new Thread(new Runnable() {
        @Override
        public void run() {
            handlerThread = new HandlerThread("子线程Handler");
            handlerThread.start();

            handler = new Handler(handlerThread.getLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    Toast.makeText(WindowActivity.this, "在子线程Toast", Toast.LENGTH_LONG).show();
                }
            };
        }
    }).start();

子线程更新UI报异常的原因在android/view/ViewRootImpl.java 中的 checkThread方法,Toast并没有继承View。

能任意设置Toast时长吗

我们在外部调用Toast.makeText进行Duration设置后,最终执行定位到TN(Toast的实际功能实现类)的handleShow方法中。

   static final long SHORT_DURATION_TIMEOUT = 4000;
   static final long LONG_DURATION_TIMEOUT = 7000;

   public void handleShow(IBinder windowToken) {

        if (mView != mNextView) {
            //删除一堆WindowManager.LayoutParams的设置
            mParams.hideTimeoutMilliseconds = mDuration ==
                Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;

            //删除另一堆WindowManager.LayoutParams的设置
            try {
            //最终将View显示在Window窗口中,这块涉及WindowManager之类的知识,就是设置View在屏幕中的x,y和层级
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            } catch (WindowManager.BadTokenException e) {
                /* ignore */
            }
        }
    }

通过关键代码看到无论设置什么值,最终都是显示4s或者7s(ps:印象中是2s和5s???)

上面搞错了,hideTimeoutMilliseconds是为了防止View长时间存在造成遮挡的措施。

通过调用show方法将TN放入队列中

 public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

找到INotificationManager的实现类\android\server\notification\NotificationManagerService.java

    public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
        ......

        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index;
                //若队列为空,封装为ToastRecord入队。队列中有就取出复用
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                    try {
                        record.callback.hide();
                    } catch (RemoteException e) {
                    }
                    record.update(callback);
                } else {
                    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;
                }
                keepProcessAliveIfNeededLocked(callingPid);
                //显示Toast的方法在这
                if (index == 0) {
                    showNextToastLocked();
                }
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }
    }

又将Toast封装成ToastRecord。在showNextToastLock方法中取出执行scheduleDurationReachedLocked(record),最终找到设置时长的地方。

//com/android/server/policy/PhoneWindowManager.java
public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds

static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
static final int SHORT_DELAY = 2000; // 2 second

private void scheduleDurationReachedLocked(ToastRecord r)
{
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    mHandler.sendMessageDelayed(m, delay);
}

得Toast.LENGTH_LONG为3.5s,Toast.LENGTH_SHORT为2s,仅支持这两种时长。

是否能够无限Toast

在调用show方法后会将TN存入INotificationManager的队列中,找到INotificationManager的实现类\android\server\notification\NotificationManagerService.java

static final int MAX_PACKAGE_NOTIFICATIONS = 50;
private boolean checkDisqualifyingFeatures(int userId, int callingUid, int id, String tag,
        NotificationRecord r, boolean isAutogroup) {

    //判断是否系统应用
    final String pkg = r.sbn.getPackageName();
    final boolean isSystemNotification =
            isUidSystemOrPhone(callingUid) || ("android".equals(pkg));
    final boolean isNotificationFromListener = mListeners.isListenerPackage(pkg);

    if (!isSystemNotification && !isNotificationFromListener) {
        synchronized (mNotificationLock) {


            ......
            // 限制入队的速率
            if (mNotificationsByKey.get(r.sbn.getKey()) != null
                    && !r.getNotification().hasCompletedProgress()
                    && !isAutogroup) {

                final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
                if (appEnqueueRate > mMaxPackageEnqueueRate) {
                    mUsageStats.registerOverRateQuota(pkg);
                    final long now = SystemClock.elapsedRealtime();
                    if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
                        Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
                                + ". Shedding " + r.sbn.getKey() + ". package=" + pkg);
                        mLastOverRateLogTime = now;
                    }
                    return false;
                }
            }

            // 限制队列中的最大数量为50
            int count = getNotificationCountLocked(pkg, userId, id, tag);
            if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                mUsageStats.registerOverCountQuota(pkg);
                Slog.e(TAG, "Package has already posted or enqueued " + count
                        + " notifications.  Not showing more.  package=" + pkg);
                return false;
            }
        }
    }

    ......

    return true;
}

其他

文中提到的HandlerThread、WindowManger还有Ibinder,AIDL实现IPC(跨进程通信)一堆的东西要学才能彻底搞懂Toast实现啊!!!

相关文章

网友评论

      本文标题:Toast几个面试题与代码导读

      本文链接:https://www.haomeiwen.com/subject/inpashtx.html