简单源码分析之小小的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分析
下回分解吧