1. 程式人生 > >Android中通過反射來設置Toast的顯示時間

Android中通過反射來設置Toast的顯示時間

ring margin ner manage etc short 延遲 lln sse

這個Toast的顯示在Android中的用途還是非常大的,同一時候我們也知道toast顯示的時間是不可控的。我們僅僅能改動他的顯示樣式和顯示的位置,盡管他提供了一個顯示時間的設置方法。可是那是沒有效果的(後面會說到)。他有兩個靜態的常量Toast.SHORT和Toast.LONG,這個在後面我會在源碼中看到這個兩個時間事實上是2.5s和3s。

那麽我們假設真想控制toast的顯示時間該怎麽辦呢?真的是無計可施了嗎?天無絕人之路,並且Linux之父以前說過:遇到問題就去看那個操蛋的源碼吧。!以下就從源碼開始分析怎麽設置toast的顯示時間的。


Toast的源碼:
我們尋常使用的makeText方法:


/**
     * Make a standard toast that just contains a text view.
     *
     * @param context  The context to use.  Usually your [email protected] android.app.Application}
     *                 or [email protected] android.app.Activity} object.
     * @param text     The text to show.  Can be formatted text.
     * @param duration How long to display the message.  Either [email protected]
/* */ #LENGTH_SHORT} or * [email protected] #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:

<?

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
        }
    }

這種方法非常easy的,首先獲取一個服務,然後將我們須要顯示的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;
            }
        };
再看方法:handleShow
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中的。

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。我猜原因非常easy,由於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;
               }
            }
        });
        
    }
}

通過一個button能夠控制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;

/** [email protected]} */
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標誌為true


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。

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中通過反射來設置Toast的顯示時間