ActivityTaskView: 直观的Activity任务栈和LaunchMode分析工具

新版使用方法

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

  1. 安装ActivityTaskView release app,启动并给予悬浮窗权限
    https://github.com/rome753/ActivityTaskView/releases
    或者从 Google Play下载安装。

  2. 在你开发的App中加入如下类https://github.com/rome753/ActivityTaskView/blob/master/app/src/main/java/cc/rome753/demo/ActivityTaskHelper.java

  3. 在你开发的App的Application的onCreate()中加入代码

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

        if(BuildConfig.DEBUG) {
            ActivityTaskHelper.init(this);
        }
    }
  1. 启动你的App,在ActivityTaskView的悬浮窗中能看到App中所有Activity和Fragment的生命周期。

LaunchMode分析

有了这个工具,分析Activity的LaunchMode就很直观了,一图胜千言。

standard mode

标准模式,启动直接加到栈顶,销毁后移除。


s.gif

singletop mode

栈顶唯一,如果栈顶存在就不会重复启动,保证栈顶不会有两个相同的Activtiy


s-to.gif

singletask mode

栈内唯一,如果栈内存在,再次启动时会自动把它上面的其他Activity全部清除(调用onDestroy)


s-ta.gif

singleinstance mode

独占一栈,启动时会建立新栈切换过去,如果启动了普通Activity又会切换回原来的共享栈(新栈仍然存在,会在栈内唯一的Activity结束时关闭)


s-in.gif

技术实现

Activity是安卓开发中最重要的元素,因为APP绝大部分使用都是操作它。某个应用的Activity都是放在一个或多个任务栈中,有两种方法可以查看任务栈和栈中的活动。

  1. ADB命令

adb shell dumpsys activity activities

该方法可以获得手机中所有活动的详细数据,然而要从中找到你想分析的活动有点麻烦,而且必须连着电脑。

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

该方法只能获取到任务栈的栈顶和栈底的活动,操作起来也麻烦。

总之,目前还没有一种方法能直观地观察Activity任务栈和Activity中的Fragment,像下图这样:


overview.gif

原理

Android4.0以后Application支持ActivityLifecycleCallbacksFragmentLifecycleCallbacks的生命周期回调。

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中注册这个回调,就能监听到所有Activity的生命周期了,再也不用往一个个Activity的生命周期方法里面加log了,在这个回调里统一搞定。

有了回调监听,就可以从APP启动开始,管理建立的每一个Activity,而Activity的getTaskId()方法可以获取到这个Activity属于哪个任务栈。

Activity和任务栈都有了,后面只是想个方法展示的问题。

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启动时可以给它注册FragmentLifecycleCallbacks来监听Activity中Fragment的变化。不同于Activity在Task中是线性的数据结构,Fragment在Activity中是树状的:一个Activity中可以有多个Fragment,每个Fragment又可以有自己的子Fragment。因此本地需要维护一个树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时便于Textview文本对齐。

悬浮窗

要在开发中直观、动态地展示任务栈,同时不能影响当前页面,使用悬浮窗是最好的方法。

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

添加悬浮窗用这个方法就可以了,加上悬浮窗权限。

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

悬浮窗加上触摸移动,自动贴边,点击切换最小化和长按回到主界面。


    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活动栈,它是从Activity中取得的。不同的App自然是不同的栈(包名不同),一个App也可以有多个栈(包名相同,hashcode不同);
中间层是活动栈中的Activity,在每个栈中是线性排列的;
最内层是Acitivity中的Fragment,一个Activity中可以有多个Fragment,一个Fragment中也可以有多个子Fragment,因此用一个简单的树来显示。

正常情况下,能准确地表示当前APP的活动栈和Fragment。除了APP被异常杀死,此时不会有生命周期回调,悬浮窗也不会刷新。

延时消息队列

很多时候Activity的生命周期转换太快,比如从onStart到onPause,或从一个Activity的onPause到另一个Activity的onResume,如果实时把这些变化反映到ActivityTaskView上,就很难看清中间的变化过程。因此在新版本中添加了一个延时消息队列,思路如下:
生命周期产生时,先将对应的信息添加到一个Queue队列中,用一个Handler从队列中取消息,如果本次取消息距上一次取消息的间隔时间小于规定DELAY,那么就等待一段时间重新取。满足时间间隔才把生命周期反映到界面上。

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

    }

广播解耦

如果每个App观察生命周期都要加依赖库、申请悬浮窗权限会很麻烦,对于一个App来说,最主要的是暴露它的生命周期,悬浮窗展示完全可以统一交给另一个App处理,这样对被观察App耦合最小。

基于这样的思路,将原来的ActivityTaskView依赖库打包成独立的App,它负责接收其他App的生命周期,展示到自己的悬浮窗上。这就涉及到两个App之间的跨进程通信了,这个需求中只需要单向通信,使用广播是最方便的。被观察App注册生命周期监听,并发送广播给ActivityTaskView,ActivityTaskView接收和解析广播,然后刷新悬浮窗UI。

现在在手机中装了ActivityTaskView的前提下,你开发的App不用添加依赖库,也不用申请悬浮窗权限,实际上只要加一个发送广播的方法,就能观察它的生命周期了。

View缓存池

加入Fragment之后,由于每个Fragment就有13个生命周期,生命周期刷新变得非常频繁,每次刷新重建视图会重复创建很多View,非常影响ActivityTaskView的性能和耗电。考虑到视图中主要都是Textview,刷新时它们仅仅是颜色文本等属性有一些变化,没必要重新创建,因此可以使用一个缓存池把它们缓存起来。视图销毁时将它们保存到ViewPool中,视图重建时再从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();
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容