新版使用方法
Github地址:https://github.com/rome753/ActivityTaskView
安裝ActivityTaskView release app,啟動并給予懸浮窗權(quán)限
https://github.com/rome753/ActivityTaskView/releases
或者從 Google Play下載安裝。在你開發(fā)的App中加入如下類https://github.com/rome753/ActivityTaskView/blob/master/app/src/main/java/cc/rome753/demo/ActivityTaskHelper.java
在你開發(fā)的App的Application的onCreate()中加入代碼
@Override
public void onCreate() {
super.onCreate();
if(BuildConfig.DEBUG) {
ActivityTaskHelper.init(this);
}
}
- 啟動你的App,在ActivityTaskView的懸浮窗中能看到App中所有Activity和Fragment的生命周期。
LaunchMode分析
有了這個工具,分析Activity的LaunchMode就很直觀了,一圖勝千言。
standard mode
標(biāo)準(zhǔn)模式,啟動直接加到棧頂,銷毀后移除。

singletop mode
棧頂唯一,如果棧頂存在就不會重復(fù)啟動,保證棧頂不會有兩個相同的Activtiy

singletask mode
棧內(nèi)唯一,如果棧內(nèi)存在,再次啟動時會自動把它上面的其他Activity全部清除(調(diào)用onDestroy)

singleinstance mode
獨(dú)占一棧,啟動時會建立新棧切換過去,如果啟動了普通Activity又會切換回原來的共享棧(新棧仍然存在,會在棧內(nèi)唯一的Activity結(jié)束時關(guān)閉)

技術(shù)實(shí)現(xiàn)
Activity是安卓開發(fā)中最重要的元素,因?yàn)锳PP絕大部分使用都是操作它。某個應(yīng)用的Activity都是放在一個或多個任務(wù)棧中,有兩種方法可以查看任務(wù)棧和棧中的活動。
- ADB命令
adb shell dumpsys activity activities
該方法可以獲得手機(jī)中所有活動的詳細(xì)數(shù)據(jù),然而要從中找到你想分析的活動有點(diǎn)麻煩,而且必須連著電腦。
- 使用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ù)棧的棧頂和棧底的活動,操作起來也麻煩。
總之,目前還沒有一種方法能直觀地觀察Activity任務(wù)棧和Activity中的Fragment,像下圖這樣:

原理
Android4.0以后Application支持ActivityLifecycleCallbacks和FragmentLifecycleCallbacks的生命周期回調(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中注冊這個回調(diào),就能監(jiān)聽到所有Activity的生命周期了,再也不用往一個個Activity的生命周期方法里面加log了,在這個回調(diào)里統(tǒng)一搞定。
有了回調(diào)監(jiān)聽,就可以從APP啟動開始,管理建立的每一個Activity,而Activity的getTaskId()方法可以獲取到這個Activity屬于哪個任務(wù)棧。
Activity和任務(wù)棧都有了,后面只是想個方法展示的問題。
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來監(jiān)聽Activity中Fragment的變化。不同于Activity在Task中是線性的數(shù)據(jù)結(jié)構(gòu),F(xiàn)ragment在Activity中是樹狀的:一個Activity中可以有多個Fragment,每個Fragment又可以有自己的子Fragment。因此本地需要維護(hù)一個樹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文本對齊。
懸浮窗
要在開發(fā)中直觀、動態(tài)地展示任務(wù)棧,同時不能影響當(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);
添加懸浮窗用這個方法就可以了,加上懸浮窗權(quán)限。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
懸浮窗加上觸摸移動,自動貼邊,點(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的視圖

視圖分為三層:
最外層是ActivityTask活動棧,它是從Activity中取得的。不同的App自然是不同的棧(包名不同),一個App也可以有多個棧(包名相同,hashcode不同);
中間層是活動棧中的Activity,在每個棧中是線性排列的;
最內(nèi)層是Acitivity中的Fragment,一個Activity中可以有多個Fragment,一個Fragment中也可以有多個子Fragment,因此用一個簡單的樹來顯示。
正常情況下,能準(zhǔn)確地表示當(dāng)前APP的活動棧和Fragment。除了APP被異常殺死,此時不會有生命周期回調(diào),懸浮窗也不會刷新。
延時消息隊列
很多時候Activity的生命周期轉(zhuǎn)換太快,比如從onStart到onPause,或從一個Activity的onPause到另一個Activity的onResume,如果實(shí)時把這些變化反映到ActivityTaskView上,就很難看清中間的變化過程。因此在新版本中添加了一個延時消息隊列,思路如下:
生命周期產(chǎn)生時,先將對應(yīng)的信息添加到一個Queue隊列中,用一個Handler從隊列中取消息,如果本次取消息距上一次取消息的間隔時間小于規(guī)定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觀察生命周期都要加依賴庫、申請懸浮窗權(quán)限會很麻煩,對于一個App來說,最主要的是暴露它的生命周期,懸浮窗展示完全可以統(tǒng)一交給另一個App處理,這樣對被觀察App耦合最小。
基于這樣的思路,將原來的ActivityTaskView依賴庫打包成獨(dú)立的App,它負(fù)責(zé)接收其他App的生命周期,展示到自己的懸浮窗上。這就涉及到兩個App之間的跨進(jìn)程通信了,這個需求中只需要單向通信,使用廣播是最方便的。被觀察App注冊生命周期監(jiān)聽,并發(fā)送廣播給ActivityTaskView,ActivityTaskView接收和解析廣播,然后刷新懸浮窗UI。
現(xiàn)在在手機(jī)中裝了ActivityTaskView的前提下,你開發(fā)的App不用添加依賴庫,也不用申請懸浮窗權(quán)限,實(shí)際上只要加一個發(fā)送廣播的方法,就能觀察它的生命周期了。
View緩存池
加入Fragment之后,由于每個Fragment就有13個生命周期,生命周期刷新變得非常頻繁,每次刷新重建視圖會重復(fù)創(chuàng)建很多View,非常影響ActivityTaskView的性能和耗電??紤]到視圖中主要都是Textview,刷新時它們僅僅是顏色文本等屬性有一些變化,沒必要重新創(chuàng)建,因此可以使用一個緩存池把它們緩存起來。視圖銷毀時將它們保存到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();
}
}