简单源码分析之小小的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()

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分析

下回分解吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容