同學,你的系統吐司可能需要修復一下
熱文導讀 | 點選標題閱讀
歡迎fork,專案地址:
https://github.com/Dovar66/DToast
先看看使用系統吐司存在的問題:
1.當通知許可權被關閉時在華為等手機上Toast不顯示;
2.Toast的佇列機制在不同手機上可能會不相同;
3.Toast的BadTokenException問題;
當發現系統吐司存在問題時,不少同學都會採用自定義的TYPE_TOAST彈窗來實現相同效果雖然大部分情況下效果都是。
OK的,但其實TYPE_TOAST彈窗依然存在相容問題:
4.Android8.0之後的token null is not valid問題(實測部分機型問題);
5.Android7.1之後,不允許同時展示兩個TYPE_TOAST彈窗(實測部分機型問題)。
那麼,DToast使用的解決方案是:
1.通知許可權未被關閉時,使用SystemToast(修復了問題2和問題3的系統Toast);
2.通知許可權被關閉時,使用DovaToast(自定義的TYPE_TOAST彈窗);
3.當使用DovaToast出現token null is not valid時,嘗試使用ActivityToast(自定義的TYPE_APPLICATION_ATTACHED_DIALOG
彈窗,只有當傳入Context為Activity時,才會啟用ActivityToast).
相信不少同學舊專案中封裝的ToastUtil都是直接使用的ApplicationContext的作為上下文,然後在需要彈窗的時候直接就是ToastUtil.show(STR),這樣的使用方式對於我們來說是最方便的啦。
當然,使用DToast你也依然可以沿用這種封裝方式,但這種方式在下面這個場景中可能會無法成功展示出彈窗(該場景下原生吐司也一樣無法彈出),
不過請放心不會導致應用崩潰,而且這個場景出現的概率較小,有以下三個必要條件:1。通知欄許可權被關閉(通知欄許可權預設都是開啟的)
2。非MIUI手機3.Android8.0以上的部分手機(我最近測試中的幾部8.0+裝置都不存在該問題)。
不過,如果想要保證在所有場景下都能正常展示彈窗,還是建議在DToast.make(上下文)時傳入活動作為上下文,這樣在該場景下DToast會啟用ActivityToast展示出彈窗。
接下來再詳細分析下上面提到的五個問題:
問題一:關閉通知許可權時吐司不顯示
看下方Toast原始碼中的show()方法,通過AIDL獲取到INotificationManager,並將接下來的顯示流程控制權
交給NotificationManagerService。
NMS中會對Toast進行許可權校驗,當通知許可權校驗不通過時,Toast將不做展示。
當然不同ROM中NMS可能會有不同,比如MIUI就對這部分內容進行了修改,所以小米手機關閉通知許可權不會導致Toast不顯示。
/**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
如何解決這個問題?只要能夠繞過NotificationManagerService即可。
DovaToast通過使用TYPE_TOAST實現全域性彈窗功能,不使用系統Toast,也沒有使用NMS服務,因此不受通知許可權限制。
問題二:系統吐司的佇列機制在不同手機上可能會不相同
我找了四臺裝置,建立兩個Gravity不同的Toast並呼叫show()方法,結果出現了四種展示效果:
* 榮耀5C-android7.0(只看到展示第一個Toast)
* 小米8-MIUI10(只看到展示第二個Toast,即新的Toast.show會中止當前Toast的展示)
* 紅米6pro-MIUI9(兩個Toast同時展示)
* 榮耀5C-android6.0(第一個TOAST展示完成後,第二個才開始展示)
造成這個問題的原因應該是各大廠商ROM中NMS維護吐司佇列的邏輯有差異。
同樣的,DToast內部也維護著自己的佇列邏輯,保證在所有手機上使用DToast的效果相同。
DToast中多個彈窗連續出現時:
1.相同優先順序時,會終止上一個,直接展示後一個;
2.不同優先順序時,如果後一個的優先順序更高則會終止上一個,直接展示後一個。
問題三:系統吐司的BadTokenException問題
Toast有個內部類TN(擴充套件ITransientNotification.Stub),呼叫Toast.show()時會將TN傳遞給NMS;
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}在NMS中會生成一個windowToken,並將windowToken給到WindowManagerService,WMS會暫時儲存該令牌並用於之後的校驗;
NotificationManagerService.java #enqueueToast原始碼:
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemToast) {
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) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}Binder token = new Binder();//生成一個token
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
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();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}然後NMS通過呼叫TN.show(windowToken)回傳令牌給TN;
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}TN使用該令牌嘗試向WindowManager中新增Toast檢視(mParams.token = windowToken);
在API25的原始碼中,吐司的WindowManager.LayoutParams引數新增了一個標記屬性,用於對新增的視窗進行校驗。
當param.token為空時,WindowManagerImpl會為其設定DefaultToken;
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
// Only use the default token if we don't have a parent window.
if (mDefaultToken != null && mParentWindow == null) {
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
// Only use the default token if we don't already have a token.
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (wparams.token == null) {
wparams.token = mDefaultToken;
}
}
}當WindowManager收到addView請求後會檢查mParams.token是否有效,若有效則新增視窗展示,否則丟擲BadTokenException異常。
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
什麼情況下windowToken會失效?
UI執行緒發生阻塞,導致TN.show()沒有及時執行,當NotificationManager的檢測超時後便會刪除WMS中的該token,即造成token失效。
如何解決?
Google在API26中修復了這個問題,即增加了try-catch:
// 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);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
因此對於8.0之前的我們也需要做相同的處理.DToast是通過反射完成這個動作,具體看下方實現:
//捕獲8.0之前Toast的BadTokenException,Google在Android 8.0的程式碼提交中修復了這個問題
private void hook(Toast toast) {
try {
Field sField_TN = Toast.class.getDeclaredField("mTN");
sField_TN.setAccessible(true);
Field sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
sField_TN_Handler.setAccessible(true);
Object tn = sField_TN.get(toast);
Handler preHandler = (Handler) sField_TN_Handler.get(tn);
sField_TN_Handler.set(tn, new SafelyHandlerWrapper(preHandler));
} catch (Exception e) {
e.printStackTrace();
}
}
public class SafelyHandlerWrapper extends Handler {
private Handler impl;
public SafelyHandlerWrapper(Handler impl) {
this.impl = impl;
}
@Override
public void dispatchMessage(Message msg) {
try {
impl.dispatchMessage(msg);
} catch (Exception e) {
}
}
@Override
public void handleMessage(Message msg) {
impl.handleMessage(msg);//需要委託給原Handler執行
}
}
問題四:Android8.0之後的令牌null無效問題
Android8.0後對視窗管理做了限制和修改,特別是TYPE_TOAST型別的視窗,必須要傳遞一個令牌用於校驗。
API25:(PhoneWindowManager.java原始碼)
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
int type = attrs.type;
outAppOp[0] = AppOpsManager.OP_NONE;
if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
return WindowManagerGlobal.ADD_INVALID_TYPE;
}
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
// Window manager will make sure these are okay.
return WindowManagerGlobal.ADD_OKAY;
}
String permission = null;
switch (type) {
case TYPE_TOAST:
// XXX right now the app process has complete control over
// this... should introduce a token to let the system
// monitor/control what they are doing.
outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
break;
case TYPE_DREAM:
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
case TYPE_QS_DIALOG:
// The window manager will check these.
break;
case TYPE_PHONE:
case TYPE_PRIORITY_PHONE:
case TYPE_SYSTEM_ALERT:
case TYPE_SYSTEM_ERROR:
case TYPE_SYSTEM_OVERLAY:
permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
break;
default:
permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
}
if (permission != null) {
...
}
return WindowManagerGlobal.ADD_OKAY;
}
API26:(PhoneWindowManager.java原始碼)
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
int type = attrs.type;
outAppOp[0] = AppOpsManager.OP_NONE;
if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
return WindowManagerGlobal.ADD_INVALID_TYPE;
}
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
// Window manager will make sure these are okay.
return ADD_OKAY;
}
if (!isSystemAlertWindowType(type)) {
switch (type) {
case TYPE_TOAST:
// Only apps that target older than O SDK can add window without a token, after
// that we require a token so apps cannot add toasts directly as the token is
// added by the notification system.
// Window manager does the checking for this.
outAppOp[0] = OP_TOAST_WINDOW;
return ADD_OKAY;
case TYPE_DREAM:
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
case TYPE_QS_DIALOG:
// The window manager will check these.
return ADD_OKAY;
}
return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
}
為了解決問題一,DovaToast不得不選擇繞過NotificationManagerService的控制,但由於windowToken是NMS生成的,
繞過NMS就無法獲取到有效的windowToken,於是作為TYPE_TOAST的DovaToast就可能陷入第四個問題。因此,DToast選擇在DovaToast出現
該問題時引入ActivityToast,在DovaToast無法正常展示時建立一個依附於活性的彈窗展示出來,不過ActivityToast只會展示在當前活動,不具有跨頁面功能。
如果說有更好的方案,那肯定是去獲取懸浮窗許可權然後改用TYPE_PHONE等型別,但懸浮窗許可權往往不容易獲取,目前來看恐怕除了微信其他APP都不能保證拿得到使用者的懸浮窗許可權。
問題五:Android7.1之後,不允許同時展示兩個TYPE_TOAST彈窗
DToast的彈窗策略就是同一時間最多隻展示一個彈窗,邏輯上就避免了此問題。因此僅捕獲該異常。
TODO清單:
增加適配應用已獲取到懸浮窗許可權的情況
考慮是否需要支援同時展示多個彈窗
其他建議
新專案做應用架構的時候可以考慮把整個應用(除閃屏頁等特殊介面外)做成只有一個活動,其他全是片段,這樣就不存在懸浮窗的問題啦。
如果能夠接受敬酒不跨介面的話,建議使用小吃吧
想進阿里嗎?快加入我們的知識星球吧,如下:
如有收穫,歡迎「分享 」
「點贊」「評論 」
媽媽常教導我,讓我養成良好習慣。這樣長大才能成為一個有用的人。良好的習慣是尊敬師長這樣長大才能成為一個有用的人。良好的習慣是尊敬師長,愛護同學,對人有禮貌;是不粗心,做事情不拖拉;還是愛護公物,不浪費糧食。為什麼呢?因為擁有良好習慣,做一個品德高尚的人,懂得尊重別人,才會得到別人的尊重。我要努力地做到這些。我有一些壞習慣,有時候學習很粗心,把一些會做的題做錯。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃飯很慢,有的時候還剩飯。我還起床磨蹭,本來應該迅速地穿好衣服,但是,我總是磨磨蹭蹭地,速度很慢。“我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!” 在上幼兒園以前,我什麼也不會幹,就連穿衣服也是媽媽給我穿好,就要上幼兒園了,這樣可不行,媽媽鍛鍊我要學會自己穿衣服。 有一天,媽媽把衣服擺在我面前,開始讓我自己穿。一開始。我又哭又叫就是不穿,還把衣服扔的滿地都是,然後坐在地上開始大哭,等了好長時間,媽媽還是不理我,我只好自己乖乖的把衣服穿好, 一出了房間門,媽媽就笑了起來,再看看我的衣服,毛衣和褲子都穿反了,我趕緊回房間又重新穿了一遍,這次穿好了,拿起外套,可是外套的扣子又扣不上了,釦子可調皮了,好像故意和我作對,我把釦子往釦眼——人類邪惡的根源;愛情——幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話:幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話“親愛的!擦乾你的眼淚,至高無上的愛情已經開啟了我們的眼界,使我們成了它的崇拜者。是它,
媽媽常教導我,讓我養成良好習慣。這樣長大才能成為一個有用的人。良好的習慣是尊敬師長這樣長大才能成為一個有用的人。良好的習慣是尊敬師長,愛護同學,對人有禮貌;是不粗心,做事情不拖拉;還是愛護公物,不浪費糧食。為什麼呢?因為擁有良好習慣,做一個品德高尚的人,懂得尊重別人,才會得到別人的尊重。我要努力地做到這些。我有一些壞習慣,有時候學習很粗心,把一些會做的題做錯。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃飯很慢,有的時候還剩飯。我還起床磨蹭,本來應該迅速地穿好衣服,但是,我總是磨磨蹭蹭地,速度很慢。“我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!” 在上幼兒園以前,我什麼也不會幹,就連穿衣服也是媽媽給我穿好,就要上幼兒園了,這樣可不行,媽媽鍛鍊我要學會自己穿衣服。 有一天,媽媽把衣服擺在我面前,開始讓我自己穿。一開始。我又哭又叫就是不穿,還把衣服扔的滿地都是,然後坐在地上開始大哭,等了好長時間,媽媽還是不理我,我只好自己乖乖的把衣服穿好, 一出了房間門,媽媽就笑了起來,再看看我的衣服,毛衣和褲子都穿反了,我趕緊回房間又重新穿了一遍,這次穿好了,拿起外套,可是外套的扣子又扣不上了,釦子可調皮了,好像故意和我作對,我把釦子往釦眼——人類邪惡的根源;愛情——幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話:“親愛的!擦乾你的眼淚,至高無上的愛情已經開啟了我們的眼界,使我們成了它的崇拜者。是它,
如你有好的文章想和大家分享歡迎投稿,直接向我投遞文章連結即可
最後,國慶福利來了,我們的知識星球已達到1000人了,之前說過到達1000人時將大大幅漲價到169元,為了反饋大家對我們的關注和厚愛,特此維持現價99元最後一天,今天后(今晚 00:00)後將漲到169元,歡迎大家加入我們的知識星球,更多星球資訊參見:
微信掃描或者點選上方二維碼領取Android\Python\AI\Java等高階進階資源
更多學習資料點選下面的“閱讀原文”獲取
相關文章
- 同學,你的系統Toast可能需要修復一下AST
- 你的電腦裝置需要修復怎麼辦 win10你的電腦裝置需要修復的解決方法Win10
- CentOS 系統修復CentOS
- win10系統開機藍屏提示你的電腦裝置需要修復如何解決Win10
- 『學了就忘』Linux啟動引導與修復 — 74、Linux系統的修復模式(光碟修復模式)Linux模式
- 電腦系統崩潰怎麼修復 修復電腦系統的三種方法
- Win10系統重置修復系統的方法Win10
- 《RedHatlinux系統修復(通過FTP進行修復)》RedhatLinuxFTP
- 如何修復ubuntu的系統引導Ubuntu
- 斷電後的系統修復(轉)
- win10系統開機藍色畫面提示你的電腦裝置需要修復如何解決Win10
- ios系統修復軟體iOS
- XFS檔案系統的備份、恢復、修復
- win10系統怎麼修復lsp_win10系統如何修復lspWin10
- linux檔案系統損壞?你只需學會這個方法就能完美修復Linux
- 接手前端新專案?這裡有些注意點你可能需要留意一下前端
- win10系統檔案受損如何修復 win10系統檔案修復的方法Win10
- 專業的iOS系統修復軟體iOS
- 怎樣用PE修復系統
- 蘋果系統修復若干辦法蘋果
- Win10系統無法啟動提示需要對其修復的解決方法Win10
- 你可能不需要VueVue
- 修復IE----Regsvr32命令修復WIN系統故障例項VR
- win10怎麼進入系統修復_一鍵修復win10系統教程Win10
- 谷歌 Pixel4 閉眼也能解鎖:修復該漏洞可能需要幾個月谷歌
- 需要介面管理的你瞭解一下?
- win10系統修復工具怎樣使用_win10系統修復工具使用步驟Win10
- Win7安全模式下如何修復系統 Win7安全模式下系統修復方法Win7模式
- 安全模式如何修復電腦 開機如何進入系統修復模式
- 檔案系統修復的一個過程
- 如何進行WindowsXP系統網路的修復Windows
- 多系統啟動選單的修復EasyBootboot
- Linux的單使用者、修復模式等在系統修復中的運用Linux模式
- 你可能需要知道的API介面文件神器API
- Linux 系統的單使用者模式、修復模式、跨控制檯登入在系統修復中的運用Linux模式
- [譯]你可能不需要ReduxRedux
- 你值得知道的Android 熱修復,以及熱修復原理Android
- 你期待已久的熱修復—Tinker熱修復整合總結