詳解Android中的Toast源碼
Toast源碼實現(xiàn)
Toast入口
我們在應(yīng)用中使用Toast提示的時候,一般都是一行簡單的代碼調(diào)用,如下所示:
[java] view plaincopyprint?在CODE上查看代碼片派生到我的代碼片
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
makeText就是Toast的入口,我們從makeText的源碼來深入理解Toast的實現(xiàn)。源碼如下(frameworks/base/core/java/android/widget/Toast.java):
public static Toast makeText(Context context, CharSequence text, int duration) {
Toast result = new Toast(context);
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;
result.mDuration = duration;
return result;
}
從makeText的源碼里,我們可以看出Toast的布局文件是transient_notification.xml,位于frameworks/base/core/res/res/layout/transient_notification.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/bright_foreground_dark"
android:shadowColor="#BB000000"
android:shadowRadius="2.75"
/>
</LinearLayout>
系統(tǒng)Toast的布局文件非常簡單,就是在垂直布局的LinearLayout里放置了一個TextView。接下來,我們繼續(xù)跟到show()方法,研究一下布局形成之后的展示代碼實現(xiàn):
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
show方法中有兩點是需要我們注意的。(1)TN是什么東東?(2)INotificationManager服務(wù)的作用。帶著這兩個問題,繼續(xù)我們Toast源碼的探索。
TN源碼
很多問題都能通過閱讀源碼找到答案,關(guān)鍵在與你是否有與之匹配的耐心和堅持。mTN的實現(xiàn)在Toast的構(gòu)造函數(shù)中,源碼如下:
public Toast(Context context) {
mContext = context;
mTN = new 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);
}
接下來,我們就從TN類的源碼出發(fā),探尋TN的作用。TN源碼如下:
private static class TN extends ITransientNotification.Stub {
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
final Handler mHandler = new Handler();
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
WindowManager mWM;
TN() {
// 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;
/// M: [ALPS00517576] Support multi-user
params.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
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;
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);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
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.removeView(mView);
}
mView = null;
}
}
}
通過源碼,我們能很明顯的看到繼承關(guān)系,TN類繼承自ITransientNotification.Stub,用于進程間通信。這里假設(shè)讀者都有Android進程間通信的基礎(chǔ)(不太熟的建議學(xué)習(xí)羅升陽關(guān)于Binder進程通信的一系列博客)。既然TN是用于進程間通信,那么我們很容易想到TN類的具體作用應(yīng)該是Toast類的回調(diào)對象,其他進程通過調(diào)用TN類的具體對象來操作Toast的顯示和消失。
TN類繼承自ITransientNotification.Stub,ITransientNotification.aidl位于frameworks/base/core/java/android/app/ITransientNotification.aidl,源碼如下:
package android.app;
/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
ITransientNotification定義了兩個方法show()和hide(),它們的具體實現(xiàn)就在TN類當(dāng)中。TN類的實現(xiàn)為:
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
這里我們就能知道,Toast的show和hide方法實現(xiàn)是基于Handler機制。而TN類中的Handler實現(xiàn)是:
final Handler mHandler = new Handler();
而且,我們在TN類中沒有發(fā)現(xiàn)任何Looper.perpare()和Looper.loop()方法。說明,mHandler調(diào)用的是當(dāng)前所在線程的Looper對象。所以,當(dāng)我們在主線程(也就是UI線程中)可以隨意調(diào)用Toast.makeText方法,因為Android系統(tǒng)幫我們實現(xiàn)了主線程的Looper初始化。但是,如果你想在子線程中調(diào)用Toast.makeText方法,就必須先進行Looper初始化了,不然就會報出java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() 。Handler機制的學(xué)習(xí)可以參考我之前寫過的一篇博客:http://blog.csdn.net/wzy_1988/article/details/38346637。
接下來,繼續(xù)跟一下mShow和mHide的實現(xiàn),它倆的類型都是Runnable。
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
可以看到,show和hide的真正實現(xiàn)分別是調(diào)用了handleShow()和handleHide()方法。我們先來看handleShow()的具體實現(xiàn):
public void handleShow() {
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
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;
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
從源碼中,我們知道Toast是通過WindowManager調(diào)用addView加載進來的。因此,hide方法自然是WindowManager調(diào)用removeView方法來將Toast視圖移除。
總結(jié)一下,通過對TN類的源碼分析,我們知道了TN類是回調(diào)對象,其他進程調(diào)用tn類的show和hide方法來控制這個Toast的顯示和消失。
NotificationManagerService
回到Toast類的show方法中,我們可以看到,這里調(diào)用了getService得到INotificationManager服務(wù),源碼如下:
private static INotificationManager sService;
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
得到INotificationManager服務(wù)后,調(diào)用了enqueueToast方法將當(dāng)前的Toast放入到系統(tǒng)的Toast隊列中。傳的參數(shù)分別是pkg、tn和mDuration。也就是說,我們通過Toast.makeText(context, msg, Toast.LENGTH_SHOW).show()去呈現(xiàn)一個Toast,這個Toast并不是立刻顯示在當(dāng)前的window上,而是先進入系統(tǒng)的Toast隊列中,然后系統(tǒng)調(diào)用回調(diào)對象tn的show和hide方法進行Toast的顯示和隱藏。
這里INofiticationManager接口的具體實現(xiàn)類是NotificationManagerService類,位于frameworks/base/services/java/com/android/server/NotificationManagerService.java。
首先,我們來分析一下Toast入隊的函數(shù)實現(xiàn)enqueueToast,源碼如下:
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
// packageName為null或者tn類為null,直接返回,不進隊列
if (pkg == null || callback == null) {
return ;
}
// (1) 判斷是否為系統(tǒng)Toast
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
// 判斷當(dāng)前toast所屬的pkg是否為系統(tǒng)不允許發(fā)生Toast的pkg.NotificationManagerService有一個HashSet數(shù)據(jù)結(jié)構(gòu),存儲了不允許發(fā)生Toast的包名
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid()) && !areNotificationsEnabledForPackageInt(pkg)) {
if (!isSystemToast) {
return;
}
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
// (2) 查看該Toast是否已經(jīng)在隊列當(dāng)中
int index = indexOfToastLocked(pkg, callback);
// 如果Toast已經(jīng)在隊列中,我們只需要更新顯示時間即可
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// 非系統(tǒng)Toast,每個pkg在當(dāng)前mToastQueue中Toast有總數(shù)限制,不能超過MAX_PACKAGE_NOTIFICATIONS
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;
}
}
}
}
// 將Toast封裝成ToastRecord對象,放入mToastQueue中
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
// (3) 將當(dāng)前Toast所在的進程設(shè)置為前臺進程
keepProcessAliveLocked(callingPid);
}
// (4) 如果index為0,說明當(dāng)前入隊的Toast在隊頭,需要調(diào)用showNextToastLocked方法直接顯示
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
可以看到,我對上述代碼做了簡要的注釋。代碼相對簡單,但是還有4點標(biāo)注代碼需要我們來進一步探討。
(1) 判斷是否為系統(tǒng)Toast。如果當(dāng)前Toast所屬的進程的包名為“android”,則為系統(tǒng)Toast,否則還可以調(diào)用isCallerSystem()方法來判斷。該方法的實現(xiàn)源碼為:
boolean isUidSystem(int uid) {
final int appid = UserHandle.getAppId(uid);
return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
}
boolean isCallerSystem() {
return isUidSystem(Binder.getCallingUid());
}
isCallerSystem的源碼也比較簡單,就是判斷當(dāng)前Toast所屬進程的uid是否為SYSTEM_UID、0、PHONE_UID中的一個,如果是,則為系統(tǒng)Toast;如果不是,則不為系統(tǒng)Toast。
是否為系統(tǒng)Toast,通過下面的源碼閱讀可知,主要有兩點優(yōu)勢:
系統(tǒng)Toast一定可以進入到系統(tǒng)Toast隊列中,不會被黑名單阻止。
系統(tǒng)Toast在系統(tǒng)Toast隊列中沒有數(shù)量限制,而普通pkg所發(fā)送的Toast在系統(tǒng)Toast隊列中有數(shù)量限制。
(2) 查看將要入隊的Toast是否已經(jīng)在系統(tǒng)Toast隊列中。這是通過比對pkg和callback來實現(xiàn)的,具體源碼如下所示:
private int indexOfToastLocked(String pkg, ITransientNotification callback)
{
IBinder cbak = callback.asBinder();
ArrayList<ToastRecord> list = mToastQueue;
int len = list.size();
for (int i=0; i<len; i++) {
ToastRecord r = list.get(i);
if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
return i;
}
}
return -1;
}
通過上述代碼,我們可以得出一個結(jié)論,只要Toast的pkg名稱和tn對象是一致的,則系統(tǒng)把這些Toast認為是同一個Toast。
(3) 將當(dāng)前Toast所在進程設(shè)置為前臺進程。源碼如下所示:
private void keepProcessAliveLocked(int pid)
{
int toastCount = 0; // toasts from this pid
ArrayList<ToastRecord> list = mToastQueue;
int N = list.size();
for (int i=0; i<N; i++) {
ToastRecord r = list.get(i);
if (r.pid == pid) {
toastCount++;
}
}
try {
mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0);
} catch (RemoteException e) {
// Shouldn't happen.
}
}
這里的mAm=ActivityManagerNative.getDefault(),調(diào)用了setProcessForeground方法將當(dāng)前pid的進程置為前臺進程,保證不會系統(tǒng)殺死。這也就解釋了為什么當(dāng)我們finish當(dāng)前Activity時,Toast還可以顯示,因為當(dāng)前進程還在執(zhí)行。
(4) index為0時,對隊列頭的Toast進行顯示。源碼如下:
private void showNextToastLocked() {
// 獲取隊列頭的ToastRecord
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
// 調(diào)用Toast的回調(diào)對象中的show方法對Toast進行展示
record.callback.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);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
這里Toast的回調(diào)對象callback就是tn對象。接下來,我們看一下,為什么系統(tǒng)Toast的顯示時間只能是2s或者3.5s,關(guān)鍵在于scheduleTimeoutLocked方法的實現(xiàn)。原理是,調(diào)用tn的show方法展示完Toast之后,需要調(diào)用scheduleTimeoutLocked方法來將Toast消失。(如果大家有疑問:不是說tn對象的hide方法來將Toast消失,為什么要在這里調(diào)用scheduleTimeoutLocked方法將Toast消失呢?是因為tn類的hide方法一執(zhí)行,Toast立刻就消失了,而平時我們所使用的Toast都會在當(dāng)前Activity停留幾秒。如何實現(xiàn)停留幾秒呢?原理就是scheduleTimeoutLocked發(fā)送MESSAGE_TIMEOUT消息去調(diào)用tn對象的hide方法,但是這個消息會有一個delay延遲,這里也是用了Handler消息機制)。
private static final int LONG_DELAY = 3500; // 3.5 seconds
private static final int SHORT_DELAY = 2000; // 2 seconds
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
首先,我們看到這里并不是直接發(fā)送了MESSAGE_TIMEOUT消息,而是有個delay的延遲。而delay的時間從代碼中“l(fā)ong delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;”看出只能為2s或者3.5s,這也就解釋了為什么系統(tǒng)Toast的呈現(xiàn)時間只能是2s或者3.5s。自己在Toast.makeText方法中隨意傳入一個duration是無作用的。
接下來,我們來看一下WorkerHandler中是如何處理MESSAGE_TIMEOUT消息的。mHandler對象的類型為WorkerHandler,源碼如下:
private final class WorkerHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
}
}
}
可以看到,WorkerHandler對MESSAGE_TIMEOUT類型的消息處理是調(diào)用了handlerTimeout方法,那我們繼續(xù)跟蹤handleTimeout源碼:
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
handleTimeout代碼中,首先判斷當(dāng)前需要消失的Toast所屬ToastRecord對象是否在隊列中,如果在隊列中,則調(diào)用cancelToastLocked(index)方法。真相就要浮現(xiàn)在我們眼前了,繼續(xù)跟蹤源碼:
private void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
// don't worry about this, we're about to remove it from
// the list anyway
}
mToastQueue.remove(index);
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked();
}
}
哈哈,看到這里,我們回調(diào)對象的hide方法也被調(diào)用了,同時也將該ToastRecord對象從mToastQueue中移除了。到這里,一個Toast的完整顯示和消失就講解結(jié)束了。
- android之自定義Toast使用方法
- Android中使用Toast.cancel()方法優(yōu)化toast內(nèi)容顯示的解決方法
- Android控件系列之Toast使用介紹
- Android開發(fā)技巧之永不關(guān)閉的Toast信息框(長時間顯示而非系統(tǒng)關(guān)閉)
- android自定義toast(widget開發(fā))示例
- android開發(fā)教程之實現(xiàn)toast工具類
- 如何解決android Toast重復(fù)顯示
- Android AndBase框架內(nèi)部封裝實現(xiàn)進度框、Toast框、彈出框、確認框(二)
- Android編程經(jīng)典代碼集錦(復(fù)制,粘貼,瀏覽器調(diào)用,Toast顯示,自定義Dialog等)
- Android 5.0以上Toast不顯示的解決方法
相關(guān)文章
微服務(wù)?Spring?Boot?整合?Redis?BitMap?實現(xiàn)?簽到與統(tǒng)計功能
這篇文章主要介紹了微服務(wù)?Spring?Boot?整合?Redis?BitMap?實現(xiàn)?簽到與統(tǒng)計功能,文章簡單介紹了Redis BitMap 基本用法結(jié)合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2023-01-01
SpringBoot中MyBatis-Plus 查詢時排除某些字段的操作方法
這篇文章主要介紹了SpringBoot中MyBatis-Plus 查詢時排除某些字段的操作方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08
Java Builder模式構(gòu)建MAP/LIST的實例講解
下面小編就為大家?guī)硪黄狫ava Builder模式構(gòu)建MAP/LIST的實例講解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10

