简单源码分析之小小的Toast

前言: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();
/**
 * @param context  The context to use.  Usually your {@link android.app.Application}       《=== 注解只留这一句,上下文的类型
 *                 or {@link android.app.Activity} object.
 */
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);
}

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) 《=== stringId youknow
                            throws Resources.NotFoundException {
    return makeText(context, context.getResources().getText(resId), 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这货终于知道了,就是一个注册的远程服务,我们拿到一个他的代理(可以调用它的实现方法)
                                                               ==== 下面是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”, 这里就是使用的它
    3.弹toast为什么要跨进程通讯?自我理解:比如我需要在远程服务里弹toast,(有大神给答案那就太好了)一定得跨进程,关于mWM.addView暂留1
    4.binder机制,去看看罗神的博客吧
    return sService;
}


//ServiceManager中的方法,c的过来,方便理解
public static IBinder getService(String name) {
    try {
        IBinder service = sCache.get(name);  《=== sCache是一个hashmap private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
        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<N; 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<PackageUserState> userState = new SparseArray<PackageUserState>();
//.....跑偏的太厉害了,PackageUserState这个玩意源码先暂留3, 直译:包用户状态


//Toast队列,记录Toast
final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();


//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都知道,我就不胡说了~
final Context mContext;
final TN mTN;
int mDuration;
View mNextView;


public void cancel() {
    mTN.cancel();
}

public void setView(View view) {
    mNextView = view;
}

public View getView() {
    return mNextView;
}

public void setMargin(float horizontalMargin, float verticalMargin) {
    mTN.mHorizontalMargin = horizontalMargin;
    mTN.mVerticalMargin = verticalMargin;
}

public float getHorizontalMargin() {
    return mTN.mHorizontalMargin;
}

public float getVerticalMargin() {
    return mTN.mVerticalMargin;
}

public void setGravity(int gravity, int xOffset, int yOffset) {
    mTN.mGravity = gravity;
    mTN.mX = xOffset;
    mTN.mY = yOffset;
}

public int getGravity() {
    return mTN.mGravity;
}

public int getXOffset() {
    return mTN.mX;
}

public int getYOffset() {
    return mTN.mY;
}

public WindowManager.LayoutParams getWindowParams() {
    return mTN.mParams;
}


/**
 * Update the text in a Toast that was previously created using one of the makeText() methods.
 * @param resId The new text for the Toast.
 */
public void setText(@StringRes int resId) {
    setText(mContext.getText(resId));
}

/**
 * Update the text in a Toast that was previously created using one of the makeText() methods.
 * @param s The new text for the Toast.
 */
public void setText(CharSequence s) {
    if (mNextView == null) {
        throw new RuntimeException("This Toast was not created with Toast.makeText()");
    }
    TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
    if (tv == null) {
        throw new RuntimeException("This Toast was not created with Toast.makeText()");
    }
    tv.setText(s);
}

}

源码贴不上了,删了

小结:
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分析
下回分解吧

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值