您的位置:首页 > 移动开发 > Android开发

Android中通过反射来设置Toast的显示时间

2015-05-12 12:19 771 查看
这个Toast的显示在Android中的用途还是很大的,同时我们也知道toast显示的时间是不可控的,我们只能修改他的显示样式和显示的位置,虽然他提供了一个显示时间的设置方法,但是那是没有效果的(后面会说到),他有两个静态的常量Toast.SHORT和Toast.LONG,这个在后面我会在源码中看到这个两个时间其实是2.5s和3s。那么我们如果真想控制toast的显示时间该怎么办呢?真的是无计可施了吗?天无绝人之路,而且Linux之父曾经说过:遇到问题就去看那个操蛋的源代码吧!!下面就从源代码开始分析怎么设置toast的显示时间的。

Toast的源代码:

我们平常使用的makeText方法:

[java] view
plaincopy





/**

* 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;

}

这里面蕴含了很多的信息的,从这里面我们可以知道Toast显示的布局文件时transient_notification.xml,关于这个文件,我们可以在源码目录中搜索一下transient_notification.xml:

[html] view
plaincopy





<?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方法,我们可以自定义样式来进行显示的:

[java] view
plaincopy





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方法吧:

[java] view
plaincopy





/**

* 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放到这个服务的队列中进行显示,那么这里最主要的方法就是:

[java] view
plaincopy





service.enqueueToast(pkg, tn, mDuration);

首先看一下这个方法的参数是:pkg:包名,mDuration:显示的时间,tn:显示回调的包装类

这里我们可以看到其实最重要的参数是tn了,因为显示的逻辑可能就在这个类里面,找到源代码:

[java] view
plaincopy





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文件,我们打开看一下:

[java] view
plaincopy





/* //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,那就看他的实现了,回到上面的代码中:

[java] view
plaincopy





/**

* 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:

[java] view
plaincopy





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;

}

};

再看方法:handleShow

[java] view
plaincopy





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();

}

}

看一下TN的构造方法:

这个方法主要是来调节toast的显示位置,同时我们可以看到这个显示使用的是WindowManager控件,将我们toast的显示的视图view放到WindowManger中的。

[java] view
plaincopy





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

[java] view
plaincopy





final TN mTN;

擦,是包访问权限,不是public的,这时候就要借助强大的技术,反射了,我们只需要反射出这个变量,然后强暴她一次即可,得到这个变量我们可以得到这个TN类对象了,然后再使用反射获取他的show和hide方法即可,下面我们就来看一下实际的代码吧:

[java] view
plaincopy





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方法的执行了,下面是测试代码:

[java] view
plaincopy





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的显示时间,但是到这里我们还没有完,反正都看到源码了,那个核心的入队列的方法何不也看看呢?

[java] view
plaincopy





INotificationManager service = getService();

String pkg = mContext.getPackageName();

TN tn = mTN;

tn.mNextView = mNextView;

try {

service.enqueueToast(pkg, tn, mDuration);

} catch (RemoteException e) {

// Empty

}

话说要是想找到这个方法还真是有点难度(反正我是找的好蛋疼,但是这次我也找到了规律了),看一下getService方法:

[java] view
plaincopy





static private INotificationManager getService() {

if (sService != null) {

return sService;

}

sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));

return sService;

}

看到这里用使用了AIDL,当然我们可以在源代码中搜一下INotificationManager:

[java] view
plaincopy





/* //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入系统队列的方法,源码如下:

[java] view
plaincopy





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中。

下面来看一下这个方法的逻辑吧:

[java] view
plaincopy





final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));

首先,会判断pkg是否为android,如果为android的话,则表示为系统的包名,是系统Toast,则将isSystemToast标志为true。

[java] view
plaincopy





// same as isUidSystem(int, int) for the Binder caller's UID.

boolean isCallerSystem() {

return isUidSystem(Binder.getCallingUid());

}

判断当前的应用用到的uid是不是系统的,如果是系统的isSystemToast标志为true

[java] view
plaincopy





if (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,

[java] view
plaincopy





int index = indexOfToastLocked(pkg, callback);

看一下indexOfToastLocked方法:

[java] view
plaincopy





// 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方法中看到:

[java] view
plaincopy





TN tn = mTN;

这里的mTN是类变量,他是在Toast构造方法中进行实例化的。

[java] view
plaincopy





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对应的进程为前台进程,保证不被销毁,

[java] view
plaincopy





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的代码:

[java] view
plaincopy





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进行显示

[java] view
plaincopy





record.callback.show();

scheduleTimeoutLocked(record);

我们看到会调用回调对象中的show方法进行显示(这个回调对象就是我们之前说的TN对象)

我们再来看一下scheduleTimeoutLocked方法:

[java] view
plaincopy





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,但是他值是只有两个值:

[java] view
plaincopy





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文件夹中。以后遇到问题还是先看源代码,虽然代码看起来很蛋疼,但是这也是没办法的!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: