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

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

這個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 {@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:
<?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;
            }
        };
再看方法: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,我猜原因很簡單,因為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標誌為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應用安全防護和逆向分析》

點選立即購買:京東  天貓

更多內容:點選這裡

關注微信公眾號,最新技術乾貨實時推送

編碼美麗技術圈微信掃一掃進入我的"技術圈"世界
掃一掃加小編微信
新增時請註明:“編碼美麗”非常感謝!