ActivityTaskView: 直觀的Activity任務(wù)棧和LaunchMode分析工具

新版使用方法

Github地址:https://github.com/rome753/ActivityTaskView

  1. 安裝ActivityTaskView release app攘蔽,啟動(dòng)并給予懸浮窗權(quán)限
    https://github.com/rome753/ActivityTaskView/releases
    或者從 Google Play下載安裝通惫。

  2. 在你開發(fā)的App中加入如下類https://github.com/rome753/ActivityTaskView/blob/master/app/src/main/java/cc/rome753/demo/ActivityTaskHelper.java

  3. 在你開發(fā)的App的Application的onCreate()中加入代碼

    @Override
    public void onCreate() {
        super.onCreate();

        if(BuildConfig.DEBUG) {
            ActivityTaskHelper.init(this);
        }
    }
  1. 啟動(dòng)你的App仓犬,在ActivityTaskView的懸浮窗中能看到App中所有Activity和Fragment的生命周期廓块。

LaunchMode分析

有了這個(gè)工具蚓让,分析Activity的LaunchMode就很直觀了中姜,一圖勝千言宏粤。

standard mode

標(biāo)準(zhǔn)模式删壮,啟動(dòng)直接加到棧頂,銷毀后移除饰迹。


s.gif

singletop mode

棧頂唯一芳誓,如果棧頂存在就不會重復(fù)啟動(dòng)余舶,保證棧頂不會有兩個(gè)相同的Activtiy


s-to.gif

singletask mode

棧內(nèi)唯一啊鸭,如果棧內(nèi)存在凉夯,再次啟動(dòng)時(shí)會自動(dòng)把它上面的其他Activity全部清除(調(diào)用onDestroy)


s-ta.gif

singleinstance mode

獨(dú)占一棧媒怯,啟動(dòng)時(shí)會建立新棧切換過去,如果啟動(dòng)了普通Activity又會切換回原來的共享?xiàng)#ㄐ聴H匀淮嬖诰瘢瑫跅?nèi)唯一的Activity結(jié)束時(shí)關(guān)閉)


s-in.gif

技術(shù)實(shí)現(xiàn)

Activity是安卓開發(fā)中最重要的元素挟憔,因?yàn)锳PP絕大部分使用都是操作它钟些。某個(gè)應(yīng)用的Activity都是放在一個(gè)或多個(gè)任務(wù)棧中,有兩種方法可以查看任務(wù)棧和棧中的活動(dòng)绊谭。

  1. ADB命令

adb shell dumpsys activity activities

該方法可以獲得手機(jī)中所有活動(dòng)的詳細(xì)數(shù)據(jù)政恍,然而要從中找到你想分析的活動(dòng)有點(diǎn)麻煩,而且必須連著電腦达传。

  1. 使用ActivityManager
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> runningTaskInfoList =  am.getRunningTasks(10);
for (RunningTaskInfo runningTaskInfo : runningTaskInfoList) {
    log("id: " + runningTaskInfo.id);
    log("description: " + runningTaskInfo.description);
    log("number of activities: " + runningTaskInfo.numActivities);
    log("topActivity: " + runningTaskInfo.topActivity);
    log("baseActivity: " + runningTaskInfo.baseActivity.toString());
}

該方法只能獲取到任務(wù)棧的棧頂和棧底的活動(dòng)篙耗,操作起來也麻煩迫筑。

總之,目前還沒有一種方法能直觀地觀察Activity任務(wù)棧和Activity中的Fragment宗弯,像下圖這樣:


overview.gif

原理

Android4.0以后Application支持ActivityLifecycleCallbacksFragmentLifecycleCallbacks的生命周期回調(diào)脯燃。

ActivityLifecycleCallbacks

application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            }

            @Override
            public void onActivityStarted(Activity activity) {
            }

            @Override
            public void onActivityResumed(Activity activity) {
            }

            @Override
            public void onActivityPaused(Activity activity) {
            }

            @Override
            public void onActivityStopped(Activity activity) {
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
            }
        });

在Application中注冊這個(gè)回調(diào),就能監(jiān)聽到所有Activity的生命周期了蒙保,再也不用往一個(gè)個(gè)Activity的生命周期方法里面加log了辕棚,在這個(gè)回調(diào)里統(tǒng)一搞定。

有了回調(diào)監(jiān)聽邓厕,就可以從APP啟動(dòng)開始逝嚎,管理建立的每一個(gè)Activity,而Activity的getTaskId()方法可以獲取到這個(gè)Activity屬于哪個(gè)任務(wù)棧详恼。

Activity和任務(wù)棧都有了懈糯,后面只是想個(gè)方法展示的問題。

FragmentLifecycleCallbacks

private class FragmentLifecycleImpl extends FragmentManager.FragmentLifecycleCallbacks{

        @Override
        public void onFragmentPreAttached(FragmentManager fm, Fragment f, Context context) {
        }

        @Override
        public void onFragmentAttached(FragmentManager fm, Fragment f, Context context) {
        }

        @Override
        public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {
        }

        @Override
        public void onFragmentActivityCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {
        }

        @Override
        public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v, Bundle savedInstanceState) {
        }

        @Override
        public void onFragmentStarted(FragmentManager fm, Fragment f) {
            handleFragment(f);
        }

        @Override
        public void onFragmentResumed(FragmentManager fm, Fragment f) {
        }

        @Override
        public void onFragmentPaused(FragmentManager fm, Fragment f) {
        }

        @Override
        public void onFragmentStopped(FragmentManager fm, Fragment f) {
        }

        @Override
        public void onFragmentSaveInstanceState(FragmentManager fm, Fragment f, Bundle outState) {
        }

        @Override
        public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {
        }

        @Override
        public void onFragmentDestroyed(FragmentManager fm, Fragment f) {
        }

        @Override
        public void onFragmentDetached(FragmentManager fm, Fragment f) {
        }
    }

同理单雾,在Activity啟動(dòng)時(shí)可以給它注冊FragmentLifecycleCallbacks來監(jiān)聽Activity中Fragment的變化赚哗。不同于Activity在Task中是線性的數(shù)據(jù)結(jié)構(gòu),F(xiàn)ragment在Activity中是樹狀的:一個(gè)Activity中可以有多個(gè)Fragment硅堆,每個(gè)Fragment又可以有自己的子Fragment屿储。因此本地需要維護(hù)一個(gè)樹FTree。

FTree.java

public class FTree {

    private String tab1 = "" + '\u2502';            // |
    private String tab2 = "" + '\u2514' + '\u2500'; // |_
    private String tab3 = "" + '\u251c' + '\u2500'; // |-

    private Node root;

    public FTree() {
        root = new Node("");
    }

    public void add(List<String> list, String lifecycle) {
        lifeMap.put(list.get(0), lifecycle);
        Node node = root;
        while (!list.isEmpty()) {
            String s = list.remove(list.size() - 1);
            if (!node.children.containsKey(s)) {
                Node newNode = new Node(s);
                node.children.put(s, newNode);

            }
            node = node.children.get(s);
        }
    }

    public void remove(List<String> list) {
        if (list.isEmpty()) return;
        lifeMap.remove(list.get(0));
        Node node = root;
        while (list.size() > 1) {
            String s = list.remove(list.size() - 1);
            if (node.children.containsKey(s)) {
                node = node.children.get(s);
            } else return;
        }
        String last = list.get(list.size() - 1);
        node.children.remove(last);
    }

    public List<String> convertToList(){
        List<String> res = new ArrayList<>();
        convert(res, root, "", true);
        return res;
    }

    private void convert(List<String> res, Node node, String pre, boolean end){
        if(node != root){
            String s = pre + (end ? tab2 : tab3) + node.name;
            res.add(s);
        }
        int i = 0;
        for(Map.Entry<String, Node> entry : node.children.entrySet()){
            i++;
            boolean subEnd = i == node.children.size();
            String subPre = pre + (node == root ? "" : (end ? "        " : tab1 + "   "));
            convert(res, entry.getValue(), subPre, subEnd);
        }
    }

    private static class Node {

        String name;
        HashMap<String, Node> children;

        Node(String name) {
            this.name = name;
            this.children = new HashMap<>();
        }
    }

    public String getLifecycle(String name) {
        return lifeMap.get(name);
    }

    private HashMap<String, String> lifeMap = new HashMap<>();
    public void updateLifecycle(String key, String value) {
        lifeMap.put(key, value);
    }
}

其中子Fragment前面加上制表符號渐逃,展示到UI時(shí)便于Textview文本對齊够掠。

懸浮窗

要在開發(fā)中直觀、動(dòng)態(tài)地展示任務(wù)棧茄菊,同時(shí)不能影響當(dāng)前頁面疯潭,使用懸浮窗是最好的方法。

WindowManager windowManager = (WindowManager)app.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_PHONE;
params.format = PixelFormat.RGBA_8888;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.gravity = Gravity.START | Gravity.TOP;
params.x = 0;
params.y = app.getResources().getDisplayMetrics().heightPixels;
windowManager.addView(activityTaskView, params);

添加懸浮窗用這個(gè)方法就可以了面殖,加上懸浮窗權(quán)限竖哩。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

懸浮窗加上觸摸移動(dòng),自動(dòng)貼邊脊僚,點(diǎn)擊切換最小化和長按回到主界面相叁。


    float mInnerX;
    float mInnerY;
    long downTime;

    @Override
        public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTime = System.currentTimeMillis();
                mInnerX = event.getX();
                mInnerY = event.getY();
                postDelayed(this, 300);
                break;
            case MotionEvent.ACTION_MOVE:
                float x = event.getRawX();
                float y = event.getRawY();
                WindowManager.LayoutParams params = (WindowManager.LayoutParams) getLayoutParams();
                params.x = (int) (x - mInnerX);
                params.y = (int) (y - mInnerY - mStatusHeight);
                updateLayout(params);

                if(Math.abs(event.getX() - mInnerX) > 20
                        || Math.abs(event.getY() - mInnerY) > 20) {
                    removeCallbacks(this);
                }
                break;
            case MotionEvent.ACTION_UP:
                removeCallbacks(this);
                if(System.currentTimeMillis() - downTime < 100
                        && Math.abs(event.getX() - mInnerX) < 20
                        && Math.abs(event.getY() - mInnerY) < 20) {
                    doClick();
                }
                moveToBorder();
                break;

        }
        return true;
    }

    private void doClick() {
        boolean visible = mTaskView.getVisibility() == VISIBLE;
        mTaskView.setVisibility(visible ? GONE : VISIBLE);
        mTinyView.setVisibility(!visible ? GONE : VISIBLE);
    }

    private void doLongClick() {
        Intent intent = new Intent(getContext().getApplicationContext(), MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getContext().getApplicationContext().startActivity(intent);
    }

    private void updateLayout(WindowManager.LayoutParams params){
        WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        if(windowManager != null) {
            windowManager.updateViewLayout(this, params);
        }
    }

    private void moveToBorder() {
        WindowManager.LayoutParams p = (WindowManager.LayoutParams) getLayoutParams();
        Log.d("chao", "x " + p.x + " " + ((mScreenWidth - getWidth()) / 2));

        if(p.x <= (mScreenWidth - getWidth()) / 2) { // move left
            p.x = 0;
        } else { // move right
            p.x = mScreenWidth;
        }
        updateLayout(p);
    }

ActivityTaskView的視圖

overview.png

視圖分為三層:
最外層是ActivityTask活動(dòng)棧,它是從Activity中取得的辽幌。不同的App自然是不同的棧(包名不同)增淹,一個(gè)App也可以有多個(gè)棧(包名相同,hashcode不同)乌企;
中間層是活動(dòng)棧中的Activity虑润,在每個(gè)棧中是線性排列的;
最內(nèi)層是Acitivity中的Fragment加酵,一個(gè)Activity中可以有多個(gè)Fragment拳喻,一個(gè)Fragment中也可以有多個(gè)子Fragment梁剔,因此用一個(gè)簡單的樹來顯示。

正常情況下舞蔽,能準(zhǔn)確地表示當(dāng)前APP的活動(dòng)棧和Fragment荣病。除了APP被異常殺死,此時(shí)不會有生命周期回調(diào)渗柿,懸浮窗也不會刷新个盆。

延時(shí)消息隊(duì)列

很多時(shí)候Activity的生命周期轉(zhuǎn)換太快,比如從onStart到onPause朵栖,或從一個(gè)Activity的onPause到另一個(gè)Activity的onResume颊亮,如果實(shí)時(shí)把這些變化反映到ActivityTaskView上,就很難看清中間的變化過程陨溅。因此在新版本中添加了一個(gè)延時(shí)消息隊(duì)列终惑,思路如下:
生命周期產(chǎn)生時(shí),先將對應(yīng)的信息添加到一個(gè)Queue隊(duì)列中门扇,用一個(gè)Handler從隊(duì)列中取消息雹有,如果本次取消息距上一次取消息的間隔時(shí)間小于規(guī)定DELAY,那么就等待一段時(shí)間重新取臼寄。滿足時(shí)間間隔才把生命周期反映到界面上霸奕。

    private static class QueueHandler extends Handler {

        private Queue<LifecycleInfo> queue;
        private long lastTime;

        QueueHandler() {
            super(Looper.getMainLooper());
            lastTime = 0;
            queue = new LinkedList<>();
        }

        void send(LifecycleInfo info) {
            queue.add(info);
            sendEmptyMessage(0);
        }

        @Override
        public void handleMessage(Message msg) {
            if (System.currentTimeMillis() - lastTime < interval) {
                sendEmptyMessageDelayed(0, interval / 5);
            } else {
                lastTime = System.currentTimeMillis();
                LifecycleInfo info = queue.poll();
                if (info != null && activityTaskView != null) {
                    if (info.fragments != null) {
                        if (info.lifecycle.contains("PreAttach")) {
                            activityTaskView.addF(info);
                        } else if (info.lifecycle.contains("Detach")) {
                            activityTaskView.removeF(info);
                        } else {
                            activityTaskView.updateF(info);
                        }
                    } else {
                        if (info.lifecycle.contains("Create")) {
                            activityTaskView.add(info);
                        } else if (info.lifecycle.contains("Destroy")) {
                            activityTaskView.remove(info);
                        } else {
                            activityTaskView.update(info);
                        }
                    }
                }
            }
        }

    }

廣播解耦

如果每個(gè)App觀察生命周期都要加依賴庫、申請懸浮窗權(quán)限會很麻煩吉拳,對于一個(gè)App來說质帅,最主要的是暴露它的生命周期,懸浮窗展示完全可以統(tǒng)一交給另一個(gè)App處理留攒,這樣對被觀察App耦合最小煤惩。

基于這樣的思路,將原來的ActivityTaskView依賴庫打包成獨(dú)立的App炼邀,它負(fù)責(zé)接收其他App的生命周期魄揉,展示到自己的懸浮窗上。這就涉及到兩個(gè)App之間的跨進(jìn)程通信了汤善,這個(gè)需求中只需要單向通信什猖,使用廣播是最方便的。被觀察App注冊生命周期監(jiān)聽红淡,并發(fā)送廣播給ActivityTaskView,ActivityTaskView接收和解析廣播降铸,然后刷新懸浮窗UI在旱。

現(xiàn)在在手機(jī)中裝了ActivityTaskView的前提下,你開發(fā)的App不用添加依賴庫推掸,也不用申請懸浮窗權(quán)限桶蝎,實(shí)際上只要加一個(gè)發(fā)送廣播的方法驻仅,就能觀察它的生命周期了。

View緩存池

加入Fragment之后登渣,由于每個(gè)Fragment就有13個(gè)生命周期噪服,生命周期刷新變得非常頻繁,每次刷新重建視圖會重復(fù)創(chuàng)建很多View胜茧,非常影響ActivityTaskView的性能和耗電粘优。考慮到視圖中主要都是Textview呻顽,刷新時(shí)它們僅僅是顏色文本等屬性有一些變化雹顺,沒必要重新創(chuàng)建,因此可以使用一個(gè)緩存池把它們緩存起來廊遍。視圖銷毀時(shí)將它們保存到ViewPool中嬉愧,視圖重建時(shí)再從ViewPool中取出來使用。

ViewPool.java

public class ViewPool extends Observable {

    LinkedList<ATextView> pool = new LinkedList<>();
    HashMap<String,FragmentTaskView> map = new HashMap<>();

    private static ViewPool factory = new ViewPool();
    public static ViewPool get() {
        return factory;
    }

    public void recycle(ViewGroup viewGroup) {
        if(viewGroup != null) {
            for(int i = 0; i < viewGroup.getChildCount(); i++) {
                View view = viewGroup.getChildAt(i);
                if(view instanceof ATextView) {
                    AUtils.removeParent(view);
                    view.setTag(null);
                    pool.add((ATextView) view);
                } else if(view instanceof FragmentTaskView) {
                    // don't recycle
                } else if(view instanceof ViewGroup) {
                    recycle((ViewGroup) view);
                }
            }
        }
    }

    public ATextView getOne(Context context) {
        ATextView view;notifyObservers();
        if(pool.isEmpty()) {
            view = new ATextView(context);
            addObserver(view);
        } else {
            view = pool.remove();
        }
        return view;
    }

    public void notifyLifecycleChange(LifecycleInfo info) {
        setChanged();
        notifyObservers(info);
    }


    public FragmentTaskView getF(String activity) {
        return map.get(activity);
    }

    public FragmentTaskView addF(Context context, String activity) {
        FragmentTaskView view = new FragmentTaskView(context);
        map.put(activity, view);
        return view;
    }

    public void removeF(String activity) {
        map.remove(activity);
    }

    public void clearF() {
        map.clear();
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喉前,一起剝皮案震驚了整個(gè)濱河市没酣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卵迂,老刑警劉巖四康,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狭握,居然都是意外死亡闪金,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門论颅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哎垦,“玉大人,你說我怎么就攤上這事恃疯÷┥瑁” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵今妄,是天一觀的道長郑口。 經(jīng)常有香客問我,道長盾鳞,這世上最難降的妖魔是什么犬性? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮腾仅,結(jié)果婚禮上乒裆,老公的妹妹穿的比我還像新娘。我一直安慰自己推励,他們只是感情好鹤耍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布肉迫。 她就那樣靜靜地躺著,像睡著了一般稿黄。 火紅的嫁衣襯著肌膚如雪喊衫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天杆怕,我揣著相機(jī)與錄音族购,去河邊找鬼。 笑死财著,一個(gè)胖子當(dāng)著我的面吹牛联四,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撑教,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼朝墩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了伟姐?” 一聲冷哼從身側(cè)響起收苏,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎愤兵,沒想到半個(gè)月后鹿霸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秆乳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年懦鼠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屹堰。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肛冶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扯键,到底是詐尸還是另有隱情睦袖,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布荣刑,位于F島的核電站馅笙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏厉亏。R本人自食惡果不足惜董习,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叶堆。 院中可真熱鬧阱飘,春花似錦、人聲如沸虱颗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忘渔。三九已至高帖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畦粮,已是汗流浹背散址。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宣赔,地道東北人预麸。 一個(gè)月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像儒将,于是被迫代替她去往敵國和親吏祸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容