Android中通過反射來設定Toast的顯示時間
這個Toast的顯示在Android中的用途還是很大的,同時我們也知道toast顯示的時間是不可控的,我們只能修改他的顯示樣式和顯示的位置,雖然他提供了一個顯示時間的設定方法,但是那是沒有效果的(後面會說到),他有兩個靜態的常量Toast.SHORT和Toast.LONG,這個在後面我會在原始碼中看到這個兩個時間其實是2.5s和3s。那麼我們如果真想控制toast的顯示時間該怎麼辦呢?真的是無計可施了嗎?天無絕人之路,而且Linux之父曾經說過:遇到問題就去看那個操蛋的原始碼吧!!下面就從原始碼開始分析怎麼設定toast的顯示時間的。
Toast的原始碼:
我們平常使用的makeText方法:
這裡面蘊含了很多的資訊的,從這裡面我們可以知道Toast顯示的佈局檔案時transient_notification.xml,關於這個檔案,我們可以在原始碼目錄中搜索一下transient_notification.xml:/** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * */ 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; }
<?xml version="1.0" encoding="utf-8"?> <!-- /* //device/apps/common/res/layout/transient_notification.xml ** ** Copyright 2006, The Android Open Source Project ** ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** ** http://www.apache.org/licenses/LICENSE-2.0 ** ** Unless required by applicable law or agreed to in writing, software ** distributed under the License is distributed on an "AS IS" BASIS, ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** See the License for the specific language governing permissions and ** limitations under the License. */ --> <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>
看到了這個佈局是如此的簡單,裡面顯示的內容就是使用TextView來操作的,當然我們也可以修改這個佈局的,他提供了一個setView方法,我們可以自定義樣式來進行顯示的:
Toast toast = new Toast(this);
View v = LayoutInflater.from(this).inflate(R.layout.activity_main, null);
toast.setView(v);
toast.show();
R.layout.activity_main是我們自己的佈局檔案同時我們也可以看到Toast.makeText方法也會返回一個Toast,在這個方法裡我們看到他是使用系統的佈局檔案,然後在哪個TextView中進行顯示內容,同時返回這個Toast,所以如果我們想得到這個系統的顯示View可以使用這個方法得到一個Toast,然後再呼叫getView方法就可以得到了,同時我們也是可以在這個view上繼續加一下我們相加的控制元件,但是這樣做是沒必要的,這裡只是說一下。
下面接著來看一下顯示的show方法吧:
/**
* 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.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
這個方法很簡單的,首先獲取一個服務,然後將我們需要顯示的toast放到這個服務的佇列中進行顯示,那麼這裡最主要的方法就是:
service.enqueueToast(pkg, tn, mDuration);
首先看一下這個方法的引數是:pkg:包名,mDuration:顯示的時間,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;
}
/**
* 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;
}
}
}
這個類也不復雜,我們看到他繼承了一個類,這個類的形式不知道大家還熟悉嗎?我們在前面介紹遠端服務AIDL的時候看到過這種形式的類,所以我們可以看到他使用Binder機制,我們可以在原始碼中搜索一下:ITransientNotification看到了,果然是個aidl檔案,我們開啟看一下:
/* //device/java/android/android/app/ITransientNotification.aidl
**
** Copyright 2007, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.app;
/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
好吧,我們看到就是兩個方法,一個是show顯示,一個是隱藏hide,那就看他的實現了,回到上面的程式碼中: /**
* 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);
}
TN類中的實現這兩個方法,內部使用Handler機制:post一個mShow和mHide:
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;
}
};
再看方法:handleShowpublic 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();
}
}
看一下TN的構造方法:
這個方法主要是來調節toast的顯示位置,同時我們可以看到這個顯示使用的是WindowManager控制元件,將我們toast的顯示的檢視view放到WindowManger中的。
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;
}
之所以用WindowManger,我猜原因很簡單,因為WindowManager是可以獨立於Activity來顯示的,我們知道toast在我們推出Activity的時候都還可以進行顯示的。這個WindowManger用途也很廣泛的,那個360桌面清理小工具就是使用這個控制元件顯示的(後臺開啟一個service就可以了,不需要藉助Activity)。同時toast也提供了setGravity或者setMargin方法進行設定toast的顯示位置,其實這些設定就是在設定顯示view在WindowManager中的位置通過上面的知識我們或許稍微理清了思路,就是首先借助TN類,所有的顯示邏輯在這個類中的show方法中,然後再例項一個TN類變數,將傳遞到一個佇列中進行顯示,所以我們要向解決這個顯示的時間問題,那就從入佇列這部給截斷,因為一旦toast入隊列了,我們就控制不了,因為這個佇列是系統維護的,所以我們現在的解決思路是:
1、不讓toast入佇列
2、然後我們自己呼叫TN類中的show和hide方法
第一個簡單,我們不呼叫toast方法就可以了,但是第二個有點問題了,因為我們看到TN這個類是私有的,所以我們也不能例項化他的物件,但是toast類中有一個例項化物件:tn
final TN mTN;
擦,是包訪問許可權,不是public的,這時候就要藉助強大的技術,反射了,我們只需要反射出這個變數,然後強暴她一次即可,得到這個變數我們可以得到這個TN類物件了,然後再使用反射獲取他的show和hide方法即可,下面我們就來看一下實際的程式碼吧:package com.weijia.toast;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
public class ReflectToast {
Context mContext;
private Toast mToast;
private Field field;
private Object obj;
private Method showMethod, hideMethod;
public ReflectToast(Context c, View v) {
this.mContext = c;
mToast = new Toast(mContext);
mToast.setView(v);
reflectionTN();
}
public void show() {
try {
showMethod.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
}
public void cancel() {
try {
hideMethod.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
}
private void reflectionTN() {
try {
field = mToast.getClass().getDeclaredField("mTN");
field.setAccessible(true);//強暴
obj = field.get(mToast);
showMethod = obj.getClass().getDeclaredMethod("show", null);
hideMethod = obj.getClass().getDeclaredMethod("hide", null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
這裡我們例項化一個Toast物件,但是沒有呼叫showf方法,就是不讓toast入系統顯示佇列中,這樣就可以控制show方法和hide方法的執行了,下面是測試程式碼:package com.weijia.toast;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
public class MainActivity extends Activity {
ReflectToast toast;
boolean isShown = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView tView = new TextView(this);
tView.setText("ReflectToast !!!");
toast = new ReflectToast(this, tView);
findViewById(R.id.show_toast).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(isShown){
toast.cancel();
isShown = false;
}else{
toast.show();
isShown = true;
}
}
});
}
}
通過一個按鈕可以控制toast的顯示了,想顯示多長時間就顯示多長時間
執行效果:
注意:這裡有一個問題,我開始的時候用三星手機測試的,沒有任何效果,然後換成小米手機也不行,最後用模擬器測試是可以的了。具體原因還在解決中。。。
上面就通過反射技術來實現toast的顯示時間,但是到這裡我們還沒有完,反正都看到原始碼了,那個核心的入佇列的方法何不也看看呢?
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
話說要是想找到這個方法還真是有點難度(反正我是找的好蛋疼,但是這次我也找到了規律了),看一下getService方法:
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
看到這裡用使用了AIDL,當然我們可以在原始碼中搜一下INotificationManager:
/* //device/java/android/android/app/INotificationManager.aidl
**
** Copyright 2007, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.app;
import android.app.ITransientNotification;
import android.service.notification.StatusBarNotification;
import android.app.Notification;
import android.content.ComponentName;
import android.content.Intent;
import android.service.notification.INotificationListener;
/** {@hide} */
interface INotificationManager
{
void cancelAllNotifications(String pkg, int userId);
void enqueueToast(String pkg, ITransientNotification callback, int duration);
void cancelToast(String pkg, ITransientNotification callback);
void enqueueNotificationWithTag(String pkg, String basePkg, String tag, int id,
in Notification notification, inout int[] idReceived, int userId);
void cancelNotificationWithTag(String pkg, String tag, int id, int userId);
void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled);
boolean areNotificationsEnabledForPackage(String pkg, int uid);
StatusBarNotification[] getActiveNotifications(String callingPkg);
StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count);
void registerListener(in INotificationListener listener, in ComponentName component, int userid);
void unregisterListener(in INotificationListener listener, int userid);
void cancelNotificationFromListener(in INotificationListener token, String pkg, String tag, int id);
void cancelAllNotificationsFromListener(in INotificationListener token);
StatusBarNotification[] getActiveNotificationsFromListener(in INotificationListener token);
}
全是介面,這時候就蛋疼了,我們該如何去找到這些實現呢?這次我就總結了一個方法:首先這是介面:所以名字是:INotificationManager,那麼他的實現就可能是NotificationManager,我去原始碼中搜了一下發現的確有這個NotificationManager這個類,但是打開發現這個並沒有實現上面的介面,這時候就想了,其實吧,這個是AIDL,所以我們不能夠按照常規的思路去找,既然是AIDL,那麼肯定是Service有關的,所以我們去搜索NotificationMangerService(這個在我們搜NotificationManager的時候已經看到了),開啟看看:果不其然實現了INotificationManager.Stub,我們只看enqueueToast這個方法,也是toast入系統佇列的方法,原始碼如下:
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback + " duration=" + duration);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
//判斷是不是系統的包或者是系統的uid,是的話
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
}
//入佇列mToastQueue
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();//獲取當前程序id
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
//檢視這個toast是否在當前佇列中,有的話就返回索引
int index = indexOfToastLocked(pkg, callback);
//如果這個index大於等於0,說明這個toast已經在這個佇列中了,只需要更新顯示時間就可以了
//當然這裡callback是一個物件,pkg是一個String,所以比較的時候是物件的比較
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
//非系統的toast
if (!isSystemToast) {
//開始在佇列中進行計數,如果佇列中有這個toast的總數超過一定值,就不把toast放到佇列中了
//這裡使用的是通過包名來判斷的,所以說一個app應用只能顯示一定量的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) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
//將這個toast封裝成ToastRecord物件,放到佇列中
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);
}
//如果返回的索引是0,說明當前的這個存在的toast就在對頭,直接顯示
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
在Toast的TN物件中,會呼叫service.enqueueToast(String pkg,ItransientNotification callback,int duaraion)來將創建出來的Toast放入NotificationManagerService的ToastRecord佇列中。
NotificationManagerService是一個執行在SystemServer程序中的一個守護程序,Android大部分的IPC通訊都是通過Binder機制,這個守護程序像一個主管一樣,所有的下面的人都必須讓它進行排程,然後由它來進行顯示或者是隱藏。
所以說,所有的排程機制都在Service中。
下面來看一下這個方法的邏輯吧:
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
首先,會判斷pkg是否為android,如果為android的話,則表示為系統的包名,是系統Toast,則將isSystemToast標誌為true。
// same as isUidSystem(int, int) for the Binder caller's UID.
boolean isCallerSystem() {
return isUidSystem(Binder.getCallingUid());
}
判斷當前的應用用到的uid是不是系統的,如果是系統的isSystemToast標誌為trueif (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
}
接著判斷是否為系統的Toast,如果是,則繼續,如果不是,並且mBlockedPackages這個HashSet中包含這個包名的話,則會直接return,因為在NotificationManagerService中維護了這麼一個HashSet<String>物件,裡面包含一些不允許傳送Toast與Notification的包名,如果包含在這個裡面的話,則不允許顯示Notification與Toast。接著得到呼叫者的pid以及callingId,接著,通過pkg和callback得到在mToastQueue中對應的ToastRecord的index,
int index = indexOfToastLocked(pkg, callback);
看一下indexOfToastLocked方法:// lock on mToastQueue
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;
}
我們看到這裡是通過String的equals方法判斷和物件引用的判斷來得到這個toast是否存在佇列中了,那麼如果這個回撥物件(這個就是我們之前說到的TN類),是不同的例項物件的話,就可以表示不存在,我們在之前的Toast中的show方法中看到:TN tn = mTN;
這裡的mTN是類變數,他是在Toast構造方法中進行例項化的。private static final int MAX_PACKAGE_NOTIFICATIONS = 50;
如果index>=0的話,則說明這個Toast物件已經在mToastQueue中了,更新這個ToastRecord的時間,如果小於0的話,則說明沒有加進去,就需要判斷包名對應的ToastRecord的總數是否大於MAX_PACKAGE_NOTIFICATIONS,也就是50個,如果大於的話,就不允許應用再發Toast了,直接返回
如果沒返回的話,就創建出一個ToastRecord物件,接著,將這個物件加到mToatQueue中,並且得到這個ToastRecord的index,並且通過方法keepProcessAliveLocked(其方法內部是呼叫ActivityManagerService.setProcessForeground)來設定這個pid對應的程序為前臺程序,保證不被銷燬,
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.
}
}
這個方法中會通過這個pid到佇列中進行查詢屬於這個程序id的toast總數,然後將設定這個程序是守護程序,這裡我們可能會想起來就是,一個Activity退出的時候,toast還可以顯示就是這原因,因為這個後臺程序還在執行,我們可以在程式碼中測試一下,我們使用finish退出程式測試一下:
toast還在顯示,當我們使用殺死程序的方式來退出程式的時候,發現就不顯示了,
這裡額外的說一下,Android中退出程式的方法:
Android程式有很多Activity,比如說主視窗A,呼叫了子視窗B,如果在B中直接finish(), 接下里顯示的是A。在B中如何關閉整個Android應用程式呢?本人總結了幾種比較簡單的實現方法。
1. Dalvik VM的本地方法
android.os.Process.killProcess(android.os.Process.myPid()) //獲取PID
System.exit(0); //常規java、c#的標準退出法,返回值為0代表正常退出
2. 工作管理員方法
首先要說明該方法執行在Android 1.5 API Level為3以上才可以,同時需要許可權
ActivityManager am = (ActivityManager)getSystemService (Context.ACTIVITY_SERVICE);
am.restartPackage(getPackageName());
系統會將,該包下的 ,所有程序,服務,全部殺掉,就可以殺乾淨了,要注意加上
<uses-permission android:name=\"android.permission.RESTART_PACKAGES\"></uses-permission>
3. 根據Activity的宣告週期
我們知道Android的視窗類提供了歷史棧,我們可以通過stack的原理來巧妙的實現,這裡我們在A視窗開啟B視窗時在Intent中直接加入標誌 Intent.FLAG_ACTIVITY_CLEAR_TOP,這樣開啟B時將會清除該程序空間的所有Activity。
在A視窗中使用下面的程式碼呼叫B視窗
Intent intent = new Intent();
intent.setClass(Android123.this, CWJ.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); //注意本行的FLAG設定
startActivity(intent);
接下來在B視窗中需要退出時直接使用finish方法即可全部退出。
上面只是個補充知識下面接著來看如果上面的index為0的話,就說明是第一個,然後通過showNextToastLocked來顯示Toast。
下面來看一下showNextToastLocked的程式碼:
private 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();
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進行顯示record.callback.show();
scheduleTimeoutLocked(record);
我們看到會呼叫回撥物件中的show方法進行顯示(這個回撥物件就是我們之前說的TN物件)我們再來看一下scheduleTimeoutLocked方法:
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);
}
我們呢看到這裡是使用了handler中的延遲發信息來顯示toast的,這裡我們也看到了,延遲時間是duration,但是他值是隻有兩個值:private static final int LONG_DELAY = 3500; // 3.5 seconds
private static final int SHORT_DELAY = 2000; // 2 seconds
只有2s和3.5s這兩個值,所以我們在之前說過我們設定toast的顯示時間是沒有任何效果的。
總結一下,我們是從原始碼的角度來解決的問題的,而且這裡還用到了反射的相關技術(其實這個技術在後面說到靜態安裝的時候也會用到),所以說反射真是什麼都可以,在這我們上面總是說到原始碼目錄中搜索,這個原始碼下載地址很多的,我用的是:http://blog.csdn.net/jiangwei0910410003/article/details/19980459這個方法。下載下來是個一個base目錄,核心程式碼都在core資料夾中。以後遇到問題還是先看原始碼,雖然程式碼看起來很蛋疼,但是這也是沒辦法的!!
《Android應用安全防護和逆向分析》
點選立即購買:京東 天貓
更多內容:點選這裡
關注微信公眾號,最新技術乾貨實時推送
編碼美麗技術圈微信掃一掃進入我的"技術圈"世界掃一掃加小編微信
新增時請註明:“編碼美麗”非常感謝!