簡單原始碼分析之小小的Toast
阿新 • • 發佈:2019-02-09
前言: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分析
下回分解吧