Toast源码分析

源码分析系列这是第一篇,统一在此说明一下,以后的文章就不做说明了。写这写文章只是个人见解分析,当然最重要的是自我感觉源码阅读方面有所欠缺,也想通过这种方式培养一下自己阅读源码的习惯,记录一下当时的理解。如果能收集到一些好的建议和理解那就更好了。以Toast源码开始是因为Toast源码简单,容易理解,由易到难需要一个过程。

构造方法

我们从最简单的使用开始入手

Toast.makeText(context, "吐司",Toast.LENGTH_LONG).show();

先看makeText里面干了什么,寻找源码发现最终都是调用了同一个makeText方法

/**
     * Make a standard toast to display using the specified looper.
     * If looper is null, Looper.myLooper() is used.
     * @hide
     */
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
         //首先创建一个Toast对象
        Toast result = new Toast(context, looper); 
		//获取inflate对象
        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);
        
		//设置显示的view和时间
        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

代码比较简单,注释一下就不啰嗦了,接着往下看构造方法

   /**
     * Construct an empty Toast object.  You must call {@link #setView} before you
     * can call {@link #show}.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     */
    public Toast(Context context) {
        this(context, null);
    }

   /**
     * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
     * @hide
     */
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        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);
    }

如上代码所示,最终调用的还是两个参数的构造方法,第一个参数是Context,通常是Application或者Activity对象。但是这儿个人推荐Application对象,在封装处理的时候也可少传一个参数,而且还能防止静态类持有Activity对象造成的内存泄露;第二个参数是looper,可为空,如果为空则使用Looper.myLooper()。我们使用的时候没有传过第二个looper参数,那是因为通常我们都是在主线程使用,系统已经自动帮我们声明了。但是我们如果想要在非UI线程使用Toast可以么,答案是肯定的,只不过需要我们自己去声明looper,代码如下所示

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        Toast.makeText(context,"想要提示的吐司",Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
}).start();

在构造方法中我们发现有一个关键的对象TN,它是干什么用的,继续往下看发现TN是一个静态内部类继承了ITransientNotification.Stub,如图所示
在这里插入图片描述
因此我们可以了解,TN就是对ITransientNotification.Stub这个Binder对象的实现,具体实现了show()跟hide()两个方法。

Show()方法的实现

我们来看show()方法里都干了写什么,关键地方都添加了注释

   /**
     * Show the view for the specified duration.
     */
    public void show() {
    	//判断view是否为空,空则抛异常提示先setView()
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

		//通过Binder通信获取INotificationManager实例,getService()在下面
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
          //把toast放到NotificationManager队列里,同时传递了一些参数,注意看tn,以这个Binder对象作为参数是为了能跟  	NotificationManager交互
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

static private INotificationManager getService() {
		//单例获取
        if (sService != null) {
            return sService;
        }
        //这儿也是通过Binder通信获取NotificationManagerService访问接口
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

下面重点来看一下enqueueToast里是怎么实现的,注释很清晰

@Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (DBG) {
                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                        + " duration=" + duration);
            }

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            //判断是否是系统级的Toast
            final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

            if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                    (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                            || isPackageSuspended)) {
                Slog.e(TAG, "Suppressing toast from package " + pkg
                        + (isPackageSuspended
                                ? " due to package suspended by administrator."
                                : " by user request."));
                return;
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index;
                    // All packages aside from the android package can enqueue one toast at a time
                    if (!isSystemToast) {
                        index = indexOfToastPackageLocked(pkg);
                    } else {
                        index = indexOfToastLocked(pkg, callback);
                    }

                    // If the package already has a toast, we update its toast
                    // in the queue, we don't move it to the end of the queue.
                    //如果已经有toast就只更新
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                        try {
                            record.callback.hide();
                        } catch (RemoteException e) {
                        }
                        record.update(callback);
                    } else {
                    	//创建binder对象
                        Binder token = new Binder();
                        //生成一个窗口
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        //添加到队列
                        mToastQueue.add(record);
                        //重新计算index值
                        index = mToastQueue.size() - 1;
                    }
                    //将toast进程设置为前台进程
                    keepProcessAliveIfNeededLocked(callingPid);
                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
                    // new or just been updated.  Call back and tell it to show itself.
                    // If the callback fails, this will remove it from the list, so don't
                    // assume that it's valid after this.
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

接着看showNextToastLocked()

@GuardedBy("mToastQueue")
    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show(record.token);
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show notification " + record.callback + " in package " + record.pkg);
                // remove it from the list and let the process die
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

取出队列里第一个ToastRecord,如果不为空就通过callback通知显示,并通过scheduleDurationReachedLocked(record)设置定时取消,如果出现了异常,则从队列移除。
那callback是干嘛的,通过源码我们得知这儿的callback就是一个ITransientNotification类型的对象,及前面我们说的TN的Binder代理对象。那么为啥能通过它来通知显示呢,我们继续看它传递的参数token。

传递过来的Token干什么用

通过上面的分析我们知道showNextToastLocked()中通过ToastRecord.callback.show(token)通知显示,那这个token什么,它其实就是添加到队列时创建的一个Binder对象,通过这个token创建了一个Toast窗口,由于他是由NMS创建的,所以具有高优先级,所以当我们的App退出时,有时候还可以看到有Toast显示。
Token创建在这里插入图片描述
如图所示handleShow(token)里的token即使上面我们分析通知显示所传的token,继续看handleShow()里的实现

public void handleShow(IBinder windowToken) {
            ...省略一些代码
           
                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;
                   //这儿进行token的赋值操作
                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);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                //这儿添加view到WindowManager中,顺便把mParams参数传过去
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

什么时候自动销毁

显示我们已经知道了,那么什么时候Toast销毁呢,还记得上面我们分析的时候有一个scheduleDurationReachedLocked(record)方法么,看它里面的实现

@GuardedBy("mToastQueue")
    private void scheduleDurationReachedLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        //发送消息延时时间,根据Toast显示时间判断,只有两种选择LONG_DELAY和SHORT_DELAY
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        //发送消息销毁Toast
        mHandler.sendMessageDelayed(m, delay);
    }

阅读源码发现发送消息后在handleMessage里接收并调用handleDurationReached方法

private void handleDurationReached(ToastRecord record)
    {
        if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

如果index有效就执行cancelToastLocked(index)方法

@GuardedBy("mToastQueue")
    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
        	//这儿发送取消通知
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }
		//从队列移除ToastRecord
        ToastRecord lastToast = mToastQueue.remove(index);
		//从WMS中移除token
        mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                DEFAULT_DISPLAY);
        // We passed 'false' for 'removeWindows' so that the client has time to stop
        // rendering (as hide above is a one-way message), otherwise we could crash
        // a client which was actively using a surface made from the token. However
        // we need to schedule a timeout to make sure the token is eventually killed
        // one way or another.
        scheduleKillTokenTimeout(lastToast.token);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            showNextToastLocked();
        }
    }

先发送取消通知,然后从队列及窗口服务移除,最后如果队列还有其他Toast则显示下一个。至此,Toast源码分析告一段落。

注意

1.我们可以通过makeText来构造一个Toast使用,通过源码分析我们知道Toast会加到一个队列里面,所以会显示完一个再显示下一个,不能及时更新,我们可以只makeText一次,如果已经创建了就只更新来优化这个点。
2.经过上面的分析我们得知Toast是系统级别的,所以有的时候退出App的时候还能看到Toast显示,那是因为退出时WindowManager把Toast所在的进程设置为了前台进程,具体可以回顾一下keepProcessAliveIfNeededLocked方法。
3.makeText是一个静态方法,传入的Context参数如果是Activity则有可能会造成内存泄露,我们上面也说过建议用ApplicationContext。
4.Toast是可以在子线程使用的,主线程的时候系统自动帮我们进行了Looper操作,在子线程使用的时候我们只需要手动进行Looper操作就行了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值