美文网首页
简单源码分析之小小的Toast

简单源码分析之小小的Toast

作者: super超_9754 | 来源:发表于2017-11-15 20:19 被阅读0次

    前言:toast再常见不过,但是一个小小的toast居然内有乾坤,呵(w)呵(t)呵(f)

    源码如下:

    public class Toast {

    //toast显示时间    注释控制了下输入内容

    @IntDef({LENGTH_SHORT, LENGTH_LONG})

    @Retention(RetentionPolicy.SOURCE)

    public @interface Duration {}

    public static final int LENGTH_SHORT = 0;

    public static final int LENGTH_LONG = 1;

    public void setDuration(@Duration int duration) { 《=== 注释作用处  不按套路编译报错 Must be one of: Toast.LENGTH_SHORT, Toast.LENGTH_LONG

              mDuration = duration;

              mTN.mDuration = duration;

    }

    @Duration

    public int getDuration() {

             return mDuration;

    }

    //通常用法中乾坤Toast.makeText(context, "hello world", Toast.LENGTH_SHORT).show()

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {

             return makeText(context, null, text, duration);

    }

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,

                                  @NonNull CharSequence text, @Duration int duration) {

              Toast result = new Toast(context, looper);   《=== 完全就是一个有参构造的简单类,自身本不是什么view,Window(画外音:显示出来那个玩意一定是wm.add进入的)

              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;                     《=== 存为全局为了下面TN使用

               result.mDuration = duration;

               return result;

    }

    public Toast(Context context) {

               this(context, null);

    }

    public Toast(@NonNull Context context, @Nullable Looper looper) {

                mContext = context;

                mTN = new TN(context.getPackageName(), looper);     《=== 伏笔(TN重要的东东,后边分析)

                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);

    }


    public void show() {

               if (mNextView == null) {

                             throw new RuntimeException("setView must have been called");

               }

               INotificationManager service = getService();    《=== 好戏登场

    《==== 经过分析 ok INotificationManager这货终于知道了,就是一个注册的远程服务,我们拿到一个他的代理(可以调用它的实现方法)

               String pkg = mContext.getOpPackageName();

               TN tn = mTN;

               tn.mNextView = mNextView;

                try {

                          service.enqueueToast(pkg, tn, mDuration);              《=== toast任务,加入window,并加入到toast管理队列

                 } catch (RemoteException e) {

                           // Empty

                 }

      }

    INotificationManager由来

    private static INotificationManager sService;

    static private INotificationManager getService() {

                   if (sService != null) {

                               return sService;

                    }

                    sService =         INotificationManager.Stub.asInterface(ServiceManager.getService ("notification"));   《=== 证明了一切,INotificationManager是注册在ServiceManager的一个服务

    补充知识如下:

    1.android系统是一个多进程的系统,进程之间相互独立

    2.进程间通讯方式方法之一 “Binder”, 这里就是使用的Binder

    3.弹toast为什么要跨进程通讯?自我理解:比如我需要在远程服务里弹toast,(有大神给答案那就太好了)一定得跨进程

    4.binder机制,去看看罗神的博客吧

                    return sService;

    }

    ServiceManager中的方法,c的过来,方便理解

    public static IBinder getService(String name) {

               try {

                        IBinder service = sCache.get(name);  《=== sCache是一个hashmap private static HashMap sCache = new HashMap();

                       if (service != null) {

                                return service;

                       } else {

                                return Binder.allowBlocking(getIServiceManager().getService(name));   《=== 内存没有,去本地取 (从下面的分析看完,返回上面流程继续)

                        }

                 } catch (RemoteException e) {

                        Log.e(TAG, "error in getService", e);

                 }

                return null;

    }

    private static IServiceManager getIServiceManager() {

                 if (sServiceManager != null) {

                           return sServiceManager;

                  }

                  // Find the service manager

                  sServiceManager = ServiceManagerNative.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));   《=== 经过本地一些列查询,反正返回了这个服务

                  return sServiceManager;

    }

    BinderInternal中的方法,native的,爱莫能助了,并不知道在哪System.load(“cpp”);

    /**

    *但是可以看注解啊!

    *Return the global "context object" of the system.  This is usually

    *an implementation of IServiceManager, which you can use to find

    *other services.       《=== 菜鸡英语为您翻译:返回给你一个整个系统全局的上下文,这个东东实现了IServiceManager,用这个东东就可以查询你需要的service

    *补充: 根据返回值IBinder,可见系统内部也是用的Binder通讯,罗神讲的详细,什么本地服务,代理服务,什么用户空间,系统空间的。

    */

    public static final native IBinder getContextObject();

    //Binder内的方法

    public static IBinder allowBlocking(IBinder binder) {

              try {

                       if (binder instanceof BinderProxy) {

                                      ((BinderProxy) binder).mWarnOnBlocking = false;        《===  要查询的service本身就是一个代理服务ps

                       } else if (binder != null

                                       && binder.queryLocalInterface(binder.getInterfaceDescriptor()) == null)  {   《=== 判定自身不是空,还在本地没有

                                      Log.w(TAG, "Unable to allow blocking on interface " + binder);

    }

                } catch (RemoteException ignored) {

                 }

                 return binder;

    }

    // 猜想这个描述可能就是罗神分析里ProcessState中的fd,文件描述符

    public String getInterfaceDescriptor() {

               return mDescriptor;

    }

    //赋值方法

    public void attachInterface(IInterface owner, String descriptor) {

               mOwner = owner;

              mDescriptor = descriptor; //赋值过程暂留2

    }

    INotificationManager内部实现

    //1.INotificationManager.aidl

    void enqueueToast(String pkg, ITransientNotification callback, int duration);

    //2.实现NotificationManagerService

    @Override

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

    {

    ==========删除日志和安检

    final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));

    final boolean isPackageSuspended =

    isPackageSuspendedForUser(pkg, Binder.getCallingUid());           《=== 猜测最终返回当前应用是否正在运行吧

    if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&

    (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())

    || isPackageSuspended)) {                                 《=== 日志不看,这个判断就是如果当前这个toast请求,既不是远程代理,又不是系统,还不是应用交互期间,那你弹个毛

    ==== 多所一句,远程代理,远程服务弹toast

    return;

    }

    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) {                                          《=== toast数量限制,除了系统toast,防止内存泄露

    int count = 0;

    final int N = mToastQueue.size();

    for (int i=0; i

    final ToastRecord r = mToastQueue.get(i);

    if (r.pkg.equals(pkg)) {

    count++;

    if (count >= MAX_PACKAGE_NOTIFICATIONS) {         《=== 我擦一个应用才能弹50个吐司?

    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);    《=== 加入window

    record = new ToastRecord(callingPid, pkg, callback, duration, token);

    mToastQueue.add(record);                                                      《=== 加入管理队列

    index = mToastQueue.size() - 1;

    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();                                                        《=== fuck 终于找到show的逻辑了

    }

    } finally {

    Binder.restoreCallingIdentity(callingId);

    }}}

    // 检查是不是系统toast

    protected boolean isCallerSystemOrPhone() {

    return isUidSystemOrPhone(Binder.getCallingUid());     《=== 其实这个地方就证明Binder跨进程,它内部保存了进程id,包名等等信息,用来操作和判断

    }

    protected boolean isUidSystemOrPhone(int uid) {

    final int appid = UserHandle.getAppId(uid);

    return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);  《=== 系统uid 1000, phoneid 1001 ,证明0~1000都是系统应用

    }

    //检测当前包是否正在使用

    private boolean isPackageSuspendedForUser(String pkg, int uid) {

    int userId = UserHandle.getUserId(uid);

    try {

    return mPackageManager.isPackageSuspendedForUser(pkg, userId);        《=== 实现过程

    } catch (RemoteException re) {

    throw new SecurityException("Could not talk to package manager service");

    } catch (IllegalArgumentException ex) {

    // Package not found.

    return false;

    }

    }

    //interface IPackageManager.aidl  《=== 也是binder方式,要不我们放过它实现的过程?按照谷歌的性格应该有个PackageManagerService是实现类,如果查不到就放过他

    //好吧一点惊喜都不给我PackageManagerService

    @Override

    public boolean isPackageSuspendedForUser(String packageName, int userId) {

    final int callingUid = Binder.getCallingUid();

    enforceCrossUserPermission(callingUid, userId,

    true /* requireFullPermission */, false /* checkShell */,

    "isPackageSuspendedForUser for user " + userId);

    synchronized (mPackages) {

    final PackageSetting ps = mSettings.mPackages.get(packageName);

    if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {

    throw new IllegalArgumentException("Unknown target package: " + packageName);

    }

    return ps.getSuspended(userId);

    }

    }

    //。。。找啊找啊找朋友

    //PackageSetting extends PackageSettingBase.getSuspended()

    boolean getSuspended(int userId) {

    return readUserState(userId).suspended;          《==== 所以这个就是包用户状态的一个属性 悬浮,暂停

    }

    public PackageUserState readUserState(int userId) {

    PackageUserState state = userState.get(userId);

    if (state == null) {

    return DEFAULT_USER_STATE;                   《=== 默认false

    }

    state.categoryHint = categoryHint;

    return state;

    }

    private final SparseArray userState = new SparseArray();

    //.....跑偏的太厉害了,PackageUserState这个玩意源码先暂留3, 直译:包用户状态

    //Toast队列,记录Toast

    final ArrayList mToastQueue = new ArrayList();

    //show的逻辑

    @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);              《====  show出来,第二场好戏

    scheduleTimeoutLocked(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);                   《===  异常移除本次toast

    }

    keepProcessAliveIfNeededLocked(record.pid);

    if (mToastQueue.size() > 0) {

    record = mToastQueue.get(0);                 《===  上顶一个toast任务

    } else {

    record = null;                               《===  退出显示

    }

    }

    }

    }

    /*** 好累啊,终于到了第二场好戏,就是前面暂留的1 TN*/

    //其实经过上面分析,TN的快感就少了很多

    //extends ITransientNotification.Stub  《===  Binder通讯,一定会有一个ITransientNotification.aidl文件外露接口,实现就在这里

    //ITransientNotification.aidl中定义

    void show(IBinder windowToken);

    void hide();

    //实现,发现其实cancel不是复写

    private static class TN extends ITransientNotification.Stub {

    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    private static final int SHOW = 0;

    private static final int HIDE = 1;

    private static final int CANCEL = 2;

    final Handler mHandler;

    int mGravity;

    int mX, mY;

    float mHorizontalMargin;

    float mVerticalMargin;

    View mView;

    View mNextView;

    int mDuration;

    WindowManager mWM;

    String mPackageName;

    static final long SHORT_DURATION_TIMEOUT = 4000;

    static final long LONG_DURATION_TIMEOUT = 7000;

    TN(String packageName, @Nullable Looper looper) {

    // XXX This should be changed to use a Dialog, with a Theme.Toast

    // defined that sets up the layout params appropriately.

    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;

    mPackageName = packageName;

    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();

    // Don't do this in handleHide() because it is also invoked by

    // handleShow()

    mNextView = null;

    break;

    }

    case CANCEL: {

    handleHide();

    // Don't do this in handleHide() because it is also invoked by

    // handleShow()

    mNextView = null;

    try {

    getService().cancelToast(mPackageName, TN.this);

    } catch (RemoteException e) {

    }

    break;

    }

    }

    }

    };

    }

    /**

    *schedule handleShow into the right thread

    */

    @Override

    public void show(IBinder windowToken) {

    if (localLOGV) Log.v(TAG, "SHOW: " + this);

    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();

    }

    /**

    *schedule handleHide into the right thread

    */

    @Override

    public void hide() {

    if (localLOGV) Log.v(TAG, "HIDE: " + this);

    mHandler.obtainMessage(HIDE).sendToTarget();

    }

    public void cancel() {

    if (localLOGV) Log.v(TAG, "CANCEL: " + this);

    mHandler.obtainMessage(CANCEL).sendToTarget();

    }

    public void handleShow(IBinder windowToken) {

    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView

    +" mNextView=" + mNextView);

    // If a cancel/hide is pending - no need to show - at this point

    // the window token is already invalid and no need to do any work.

    if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {

    return;

    }

    if (mView != mNextView) {

    // remove the old view if necessary

    handleHide();

    mView = mNextView;

    Context context = mView.getContext().getApplicationContext();

    String packageName = mView.getContext().getOpPackageName();

    if (context == null) {

    context = mView.getContext();

    }

    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

    // We can resolve the Gravity here by using the Locale for getting

    // the layout direction

    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);

    // 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 {

    mWM.addView(mView, mParams);                《===   他强任他强,清风拂山岗  最终还是WindowManager.addView

    trySendAccessibilityEvent();

    } catch (WindowManager.BadTokenException e) {

    /* ignore */

    }

    }

    }

    private void trySendAccessibilityEvent() {

    AccessibilityManager accessibilityManager =

    AccessibilityManager.getInstance(mView.getContext());

    if (!accessibilityManager.isEnabled()) {

    return;

    }

    // treat toasts as notifications since they are used to

    // announce a transient piece of information to the user

    AccessibilityEvent event = AccessibilityEvent.obtain(

    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);

    event.setClassName(getClass().getName());

    event.setPackageName(mView.getContext().getPackageName());

    mView.dispatchPopulateAccessibilityEvent(event);

    accessibilityManager.sendAccessibilityEvent(event);

    }

    public void handleHide() {

    if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);

    if (mView != null) {

    // note: checking parent() just to make sure the view has

    // been added...  i have seen cases where we get here when

    // the view isn't yet added, so let's try not to crash.

    if (mView.getParent() != null) {

    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);

    mWM.removeViewImmediate(mView);

    }

    mView = null;

    }

    }

    }

    //剩下的只要自定义过toast都知道,我就不胡说了~

    源码贴不上了,删了,迷之缩进怎么解!!!

    小结:

    1.toast本质是inflate了一个View(有默认,也可以设置),然后通过WindowManager.addView()进行显示

    2.由系统中NotificationManagerService管理和维护这一个ToastQueue(toast队列)

    3.NotificationManagerService又通过Toast.TN,轮循回调,执行show的操作(即WindowManager.addView())

    4.toast是有数量限制的

    至此还剩三个遗留:

    1.wm.addView流程

    2.descriptor描述怎么来的

    3.packageUserState分析

    下回分解吧

    相关文章

      网友评论

          本文标题:简单源码分析之小小的Toast

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