1. 程式人生 > >Android UsageStatsService:要點解析

Android UsageStatsService:要點解析

1、UsageStatsService作用是什麼?

這是一個Android私有service,主要作用是收集使用者使用每一個APP的頻率、使用時常;

2、如何通過UsageStatsService獲取使用者使用APP的資料?

(1)必須要具備系統許可權;(APP內建在/system/app下)

(2)必須要在manifest中申明許可權:PACKAGE_USAGE_STATS;例如:
[html] view plain copy

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

(3)呼叫UsageStatsService.getAllPkgUsageStats or UsageStatsService.getPkgUsageStats 介面獲取使用者使用APP頻率:
[java] view plain copy

//相當於:IBinder oRemoteService = ServiceManager.getService("usagestats");  
           Class<?> cServiceManager = Class.forName("android.os.ServiceManager");  
           Method mGetService = cServiceManager.getMethod("getService", java.lang.String.class);  
           Object oRemoteService = mGetService.invoke(null, "usagestats");  

           // 相當於:IUsageStats mUsageStatsService = IUsageStats.Stub.asInterface(oRemoteService)  
           Class<?> cStub = Class.forName("com.android.internal.app.IUsageStats$Stub");  
           Method mUsageStatsService = cStub.getMethod("asInterface", android.os.IBinder.class);  
           Object oIUsageStats = mUsageStatsService.invoke(null, oRemoteService);  

           // 相當於:PkgUsageStats[] oPkgUsageStatsArray =mUsageStatsService.getAllPkgUsageStats();  
           Class<?> cIUsageStatus = Class.forName("com.android.internal.app.IUsageStats");  
           Method mGetAllPkgUsageStats = cIUsageStatus.getMethod("getAllPkgUsageStats", (Class[]) null);  
           Object[] oPkgUsageStatsArray = (Object[]) mGetAllPkgUsageStats.invoke(oIUsageStats, (Object[]) null);  

           //相當於  
           //for (PkgUsageStats pkgUsageStats: oPkgUsageStatsArray)  
           //{  
           //  當前APP的包名:  
           //  packageName = pkgUsageStats.packageName  
           //  當前APP的啟動次數  
           //  launchCount = pkgUsageStats.launchCount  
           //  當前APP的累計使用時間:  
           //  usageTime = pkgUsageStats.usageTime  
           //  當前APP的每個Activity的最後啟動時間  
           //  componentResumeTimes = pkgUsageStats.componentResumeTimes  
           //}  
           Class<?> cPkgUsageStats = Class.forName("com.android.internal.os.PkgUsageStats");  
           for (Object pkgUsageStats : oPkgUsageStatsArray) {  
               String packageName = (String) cPkgUsageStats.getDeclaredField("packageName").get(pkgUsageStats);  
               int launchCount = cPkgUsageStats.getDeclaredField("launchCount").getInt(pkgUsageStats);  
               long usageTime = cPkgUsageStats.getDeclaredField("usageTime").getLong(pkgUsageStats);  
               Map<String, Long > componentResumeMap = (Map<String, Long>) cPkgUsageStats.getDeclaredField("componentResumeTimes").get(pkgUsageStats);  
           }  

3、UsageStatsService工作原理是什麼?
(3-1)首先是service的初始化,主要分為兩步;
第一步是從/data/system/usagestats/usage-history.xml檔案中讀取每個APP中每個Activity最後啟動的時間;
[java] view plain copy

//初始化/data/system/usagestats/usage-history.xml  
 mHistoryFile = new AtomicFile(new File(mDir, FILE_HISTORY));  
 //解析xml檔案,將xml解析成為資料,並存儲  
 readStatsFromFile();  

第二步是從“/data/system/usagestats/usage-當前日誌”檔案中解析今天的使用記錄的資料:
[java] view plain copy

//指定檔案字首為usage,根據日誌生成檔案字尾,例如usage-20140817  
 mFileLeaf = getCurrentDateStr(FILE_PREFIX);  
 //解析檔案,為檔案流的格式,直接讀取即可  
 readStatsFromFile();  

其中“/data/system/usagestats/usage-當前日誌”檔案格式為2精製的序列化流,可直接從中讀取相應的物件即可;如果對應檔案不存在,則建立它;

(3-2)在初始化service完成後,需要組資料儲存:
從usage-history.xml檔案中解析出來的資料,放在:
[java] view plain copy

// key為包名,value為Map<String, Long>,記錄該包下的Activity名以及該Activity最後啟動時間  
 final private Map<String, Map<String, Long>> mLastResumeTimes;  

從“/data/system/usagestats/usage-當前日誌”解析出來的資料,放在:
[java] view plain copy

// String為包名,PkgUsageStatsExtended儲存相應的啟動資訊  
final private Map<String, PkgUsageStatsExtended> mStats;  

可以發現此處封裝了一個內部類PkgUsageStatsExtended做資料儲存,其實PkgUsageStatsExtended還封裝了許多操作,後面會逐步涉及,目前PkgUsageStatsExtended主要儲存了這些資訊:
[java] view plain copy

private class PkgUsageStatsExtended {  
     //每個Activity最後啟動時間  
     final HashMap<String, TimeStats> mLaunchTimes  
             = new HashMap<String, TimeStats>();  
     //當前APP啟動次數  
     int mLaunchCount;  
     //當前APP使用時間  
     long mUsageTime;  
     //當前APP最後一次呼叫onPause時間  
     long mPausedTime;  
     //當前APP最後一個呼叫onResume時間  
     long mResumedTime;  

     //更新mResumedTime為當前時間  
     void updateResume(String comp, boolean launched) {  
         if (launched) {  
             mLaunchCount ++;  
         }  
         mResumedTime = SystemClock.elapsedRealtime();  
     }  

     //更新mPausedTime為當前時間,並且使用時常=mPausedTime - mResumedTime  
     void updatePause() {  
         mPausedTime =  SystemClock.elapsedRealtime();  
         mUsageTime += (mPausedTime - mResumedTime);  
     }  

     //記錄activity次數  
     void addLaunchCount(String comp) {  
         TimeStats times = mLaunchTimes.get(comp);  
         if (times == null) {  
             times = new TimeStats();  
             mLaunchTimes.put(comp, times);  
         }  
         times.incCount();  
     }  

     //記錄activity啟動時間  
     void addLaunchTime(String comp, int millis) {  
         TimeStats times = mLaunchTimes.get(comp);  
         if (times == null) {  
             times = new TimeStats();  
             mLaunchTimes.put(comp, times);  
         }  
         times.add(millis);  
     }  
     /*more code 
     */  

 }  

(3-3)當應用啟動一個Activity時,UsageStatsService會發生什麼行為?
ActivityManagerService會呼叫UsageStatsService.noteResumeComponent方法,在該方法中會有以下操作:
[java] view plain copy

public void noteResumeComponent(ComponentName componentName) {  
        enforceCallingPermission();  
        String pkgName;  
        synchronized (mStatsLock) {  
                /*some code 
                */  
            final boolean samePackage = pkgName.equals(mLastResumedPkg);  
            //1、mIsResumed會在onResume中變為true,在onPause中變為false  
            if (mIsResumed) {  
                if (mLastResumedPkg != null) {  
                    //2、這裡是為了避免沒有呼叫onPause的情況出現,理論上不存在  
                    PkgUsageStatsExtended pus = mStats.get(mLastResumedPkg);  
                    if (pus != null) {  
                            //3、增加保護,呼叫一次updatePause  
                        pus.updatePause();  
                    }  
                }  
            }  

            final boolean sameComp = samePackage  
                    && componentName.getClassName().equals(mLastResumedComp);  

            //5、內部資料更新,記錄最後一次啟動的Activity  
            mIsResumed = true;  
            mLastResumedPkg = pkgName;  
            mLastResumedComp = componentName.getClassName();  

            PkgUsageStatsExtended pus = mStats.get(pkgName);  
            if (pus == null) {  
                pus = new PkgUsageStatsExtended();  
                mStats.put(pkgName, pus);  
            }  
            //6、更新Activity啟動時間,如果使用者是從一個App啟動進入另外一個APP,那麼需要App標識啟動次數+1  
            pus.updateResume(mLastResumedComp, !samePackage);  
            if (!sameComp) {  
                    //7、同上,Activity啟動次數+1  
                pus.addLaunchCount(mLastResumedComp);  
            }  

            Map<String, Long> componentResumeTimes = mLastResumeTimes.get(pkgName);  
            if (componentResumeTimes == null) {  
                componentResumeTimes = new HashMap<String, Long>();  
                mLastResumeTimes.put(pkgName, componentResumeTimes);  
            }  
            //8、更新componentResumeTimes  
            componentResumeTimes.put(mLastResumedComp, System.currentTimeMillis());  
        }  
    }  

(3-4)當退出一個Activity,UsageStatsService會發生什麼行為?
ActivityManagerService會呼叫UsageStatsService.notePauseComponent方法,UsageStatsService會更新當前展示Activity的OnPause時間:
[java] view plain copy

public void notePauseComponent(ComponentName componentName) {  
                /*some code 
                */  
        synchronized (mStatsLock) {  
            /*some code 
            */  
            //1、mIsResumed會在onResume中變為true,在onPause中變為false  
            mIsResumed = false;  

            PkgUsageStatsExtended pus = mStats.get(pkgName);  
            if (pus == null) {  
                // Weird some error here  
                Slog.i(TAG, "No package stats for pkg:"+pkgName);  
                return;  
            }  
            //2、更新onPause的時間  
            pus.updatePause();  
        }  
      //3、視情況而定確認是否需要將記憶體資料儲存成檔案  
    writeStatsToFile(false, false);        
}  

(3-5)現在,我們已經明白APP啟動資料如何在記憶體中流轉了,那麼什麼時候系統將資料持久化成檔案?
UsageStatsService通過writeStatsToFile方法將資料持久化成檔案,函式原型如下:
[java] view plain copy

/** 
 * 在特定條件下,將mStats或者mLastResumeTimes寫入到檔案中,由使用者操作觸發 
 * @params force  強制將mStats例項化到檔案中 
 *                
 * @params forceWriteHistoryStats 強制將mLastResumeTimes例項化到檔案中 
 */  
private void writeStatsToFile(final boolean force, final boolean forceWriteHistoryStats) {  
}  

具體觸發writeStatsToFile的時機有:
1、使用者關機,觸發writeStatsToFile(true, true);
2、使用者開啟一個新應用,觸發writeStatsToFile(false,false);
3、在notePauseComponent最後,呼叫writeStatsToFile(false,false);

writeStatsToFile觸發寫檔案操作的條件有:
1、關機強制觸發;
2、當前日期與最後一次寫檔案日期不同;
3、除使用者關機外,兩次寫檔案間隔必須在30分鐘以上;

(3-6)解除安裝APP後,已經記錄的資料會被清除;

===================================以下內容是本人修復===========================================

android 7.0上是20分鐘寫檔案,其他版本未知

public final class UsageStats implements Parcelable {

/**
 * {@hide}
 */
public String mPackageName;//包名

/**
 * {@hide}
 */
public long mBeginTimeStamp;

/**
 * {@hide}
 */
public long mEndTimeStamp;

/**
 * Last time used by the user with an explicit action (notification, activity launch).
 * {@hide}
 */
public long mLastTimeUsed;//上次開始時間

/**
 * {@hide}
 */
public long mTotalTimeInForeground;//一共使用的時間

/**
 * {@hide}
 */
public int mLaunchCount;//啟動的次數

/**
 * {@hide}
 */
public int mLastEvent;

每次啟動時會觸發 IntervalStats.java 中的update方法:

    void update(String packageName, long timeStamp, int eventType) {
        UsageStats usageStats = getOrCreateUsageStats(packageName);

        // TODO(adamlesinski): Ensure that we recover from incorrect event sequences
        // like double MOVE_TO_BACKGROUND, etc.
        if (eventType == UsageEvents.Event.MOVE_TO_BACKGROUND ||
                eventType == UsageEvents.Event.END_OF_DAY) {
            if (usageStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
                    usageStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
                    //注意:timeStamp就是上次的結束時間,usageStats.mLastTimeUsed就是上次開始時間
                usageStats.mTotalTimeInForeground += timeStamp - usageStats.mLastTimeUsed;

            }
        }

        if (isStatefulEvent(eventType)) {
            usageStats.mLastEvent = eventType;
        }

        if (eventType != UsageEvents.Event.SYSTEM_INTERACTION) {
            usageStats.mLastTimeUsed = timeStamp;
        }
        usageStats.mEndTimeStamp = timeStamp;

        if (eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
            usageStats.mLaunchCount += 1;
        }

        endTime = timeStamp;
    }