Android項目復盤3

個人主頁:https://chengang.plus/

文章將會同步到個人微信公眾號:Android部落格

3、健康數(shù)據(jù)記錄項目

這個項目遇到的主要問題是應用使用時長和使用次數(shù)不準確的問題。原因要從應用的業(yè)務邏輯以及源碼中去查找。

一般我們獲取應用使用數(shù)據(jù)詳情的方法是:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ArrayList<AppLaunchInfoBean> getAppLaunchInfoBean(long start, long end) {
    final UsageStatsManager usageStatsManager = (UsageStatsManager) mContext.getSystemService("usagestats");
    UsageEvents usageEvents = usageStatsManager.queryEvents(start, end);
    return getAppLaunchInfoBeanList(usageEvents, end);
}

3.1 業(yè)務邏輯

當每次打開應用的時候,通過上述方法去取使用數(shù)據(jù),或者每次從應用其他頁面回到首頁的時候去取,將取到的數(shù)據(jù)持久化保存到本地數(shù)據(jù)庫。

這種使用方式看起來很合理,但是測試人員總是反饋應用使用時長和次數(shù)不準確。到這里就需要從源碼找原因了。

3.2 UsageStatsManager源碼追溯

我們都知道linux從init.rc腳本啟動了Zygote,Zygote 通過fork創(chuàng)建了system_server進程,這個集成所屬的類是SystemServer,在他的run方法中啟動了一些列的系統(tǒng)服務,我們重點關注UsageStatsService何時啟動。

SystemServer

private void run() {
    mSystemServiceManager = new SystemServiceManager(mSystemContext);
    LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
    startCoreServices();
}

private void startCoreServices() {
    mSystemServiceManager.startService(UsageStatsService.class);
    mActivityManagerService.setUsageStatsManager(LocalServices.getService(UsageStatsManagerInternal.class));
}

SystemServiceManager統(tǒng)一管理系統(tǒng)服務,交給它去啟動服務,并且將啟動之后的服務交給ActivityManagerService調度。

SystemServiceManager

public <T extends SystemService> T startService(Class<T> serviceClass) {
    final String name = serviceClass.getName();
    final T service;
    Constructor<T> constructor = serviceClass.getConstructor(Context.class);
    service = constructor.newInstance(mContext);
    startService(service);
    return service;
}

public void startService(@NonNull final SystemService service) {
    // Register it.
    mServices.add(service);
    // Start it.
    long time = SystemClock.elapsedRealtime();
    try {
        service.onStart();
    } catch (RuntimeException ex) {
    }
    warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");//50ms
}

這里可以看到通過反射的方式調用了UsageStatsService的構造函數(shù),構造完成之后通過startService方法啟動這個服務:

UsageStatsService

public class UsageStatsService extends SystemService implements UserUsageStatsService.StatsUpdatedListener {
    public UsageStatsService(Context context) {
        super(context);
    }
}

//start方法比較長,只提取比較重要的方法
@Override
public void onStart() {
    //第一部分
    mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
    
    //第二部分
    mHandler = new H(BackgroundThread.get().getLooper());
    
    //第三部分
    File systemDataDir = new File(Environment.getDataDirectory(), "system");
    mUsageStatsDir = new File(systemDataDir, "usagestats");
    mUsageStatsDir.mkdirs();
    
    //第四部分
    publishLocalService(UsageStatsManagerInternal.class, new LocalService());
    publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
    
    //第五部分
    getUserDataAndInitializeIfNeededLocked(UserHandle.USER_SYSTEM, mSystemTimeSnapshot);
}

private UserUsageStatsService getUserDataAndInitializeIfNeededLocked(int userId, long currentTimeMillis) {
    UserUsageStatsService service = mUserState.get(userId);
    if (service == null) {
        service = new UserUsageStatsService(getContext(), userId, new File(mUsageStatsDir, Integer.toString(userId)), this);
        service.init(currentTimeMillis);
        mUserState.put(userId, service);
    }
    return service;
}

UsageStatsService的構造函數(shù)比較簡單,重點分析start方法:

  • 第一部分中獲取了UserManager,這個是為多用戶的情況下處理數(shù)據(jù)準備的
  • 第二部分中創(chuàng)建了一個Handler,用于處理數(shù)據(jù),比如存儲數(shù)據(jù)到磁盤,他的looper其實來自于HandlerThread,因為BackgroundThread繼承自HandlerThread。
  • 第三部分是在/data/system/目錄下創(chuàng)建一個usagestats的文件夾,用于創(chuàng)建文件存放數(shù)據(jù)。
  • 第四部分中,其實是將LocalService對象添加到LocalServices的集合中,而LocalService是UsageStatsService的內部類;publishBinderService做的事情就是將BinderService添加到ServiceManager中。BinderService的定義是:
private final class BinderService extends IUsageStatsManager.Stub {}

我們知道IUsageStatsManager.Stub是對客戶端提供的代理對象,客戶端獲取到對象進行對應的操作,而具體的操作函數(shù)就定義在BinderService覆寫的方法中。

  • 第五部分意在初始化一個UserUsageStatsService類,在初始化的時候回傳遞userId,根據(jù)這個userId創(chuàng)建對應的文件夾存儲不同用戶的數(shù)據(jù):

UserUsageStatsService

UserUsageStatsService(Context context, int userId, File usageStatsDir, StatsUpdatedListener listener) {
    mDatabase = new UsageStatsDatabase(usageStatsDir);
    mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
}

UserUsageStatsService構造函數(shù)中又創(chuàng)建了一個UsageStatsDatabase對象,以及IntervalStats類型的數(shù)組。

前者主要功能是往xml文件中寫數(shù)據(jù),后者的主要功能是處理不同時間間隔的數(shù)據(jù)。

UsageStatsDatabase

public UsageStatsDatabase(File dir) {
    mIntervalDirs = new File[] {
            new File(dir, "daily"),
            new File(dir, "weekly"),
            new File(dir, "monthly"),
            new File(dir, "yearly"),
    };
    mVersionFile = new File(dir, "version");
    mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
}

這里又分不同的時間屬性創(chuàng)建文件夾存放數(shù)據(jù)。

UserUsageStatsService最后會調用init方法,這個方法的目的是讀取已有的數(shù)據(jù),沒有相關的數(shù)據(jù)就初始化創(chuàng)建。

到這里基本的初始化工作就完成了。

3.3 客戶端獲取數(shù)據(jù)源碼追蹤

客戶端的調用代碼是:

usageStatsManager.queryEvents(start, end);

追蹤一下這個代碼的調用棧:

UsageStatsManager

public UsageEvents queryEvents(long beginTime, long endTime) {
    try {
        UsageEvents iter = mService.queryEvents(beginTime, endTime, mContext.getOpPackageName());
        if (iter != null) {
            return iter;
        }
    } catch (RemoteException e) {
    }
    return sEmptyResults;
}

這里的mServiceIUsageStatsManager類型,是服務端的操作對象,對應的是服務端UsageStatsService的內部類BinderService,也就是對應的調用其中的方法:

UsageStatsService.BinderService

@Override
public UsageEvents queryEvents(long beginTime, long endTime, String callingPackage) {
    if (!hasPermission(callingPackage)) {
        return null;
    }
    try {
        return UsageStatsService.this.queryEvents(userId, beginTime, endTime,
        obfuscateInstantApps);
    } finally {
        Binder.restoreCallingIdentity(token);
    }
}

UsageEvents queryEvents(int userId, long beginTime, long endTime, boolean shouldObfuscateInstantApps) {
    final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
    return service.queryEvents(beginTime, endTime, shouldObfuscateInstantApps);
}

queryEvents方法中調用了外部類的queryEvents方法,而在這個方法中最終是調用到了UserUsageStatsService的queryEvents方法:

UserUsageStatsService

UsageEvents queryEvents(final long beginTime, final long endTime, boolean obfuscateInstantApps) {
    final ArraySet<String> names = new ArraySet<>();
    List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
    beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
        @Override
        public void combine(IntervalStats stats, boolean mutable, List<UsageEvents.Event> accumulatedResult) {
            final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
            final int size = stats.events.size();
            for (int i = startIndex; i < size; i++) {
                UsageEvents.Event event = stats.events.get(i);
                names.add(event.mPackage);
                if (event.mClass != null) {
                    names.add(event.mClass);
                }
                accumulatedResult.add(event);
            }
        }
    });
    String[] table = names.toArray(new String[names.size()]);
    Arrays.sort(table);
    return new UsageEvents(results, table);
}

這里調用的時候如果不設置時間間隔,默認是INTERVAL_DAILY,看看具體的queryStats方法:

private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime, StatCombiner<T> combiner) {
    //第一部分
    final IntervalStats currentStats = mCurrentStats[intervalType];
    //第二部分
    List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, truncatedEndTime, combiner);
    //第三部分
    if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
        combiner.combine(currentStats, true, results);
    }
    return results;
}
  • 第一部分從當前內存中里面取,因為INTERVAL_DAILY的數(shù)據(jù)被report的時候,一開始是放到mCurrentStats里面存起來。

mCurrentStats是IntervalStats數(shù)組類型,而IntervalStats里面維護了一個EventList對象,這個對象里面持有一個ArrayList<UsageEvents.Event> mEvents,維護應用使用詳情數(shù)據(jù)。

  • 第二部分從本地磁盤的xml文件取需要的時間間隔內的數(shù)據(jù)。在取到數(shù)據(jù)之后回調combine方法將數(shù)據(jù)存放到一個List中:

UsageStatsDatabase

public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner) {
    final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
    int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
    int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
    final IntervalStats stats = new IntervalStats();
    final ArrayList<T> results = new ArrayList<>();
    for (int i = startIndex; i <= endIndex; i++) {
        final AtomicFile f = intervalStats.valueAt(i);
        UsageStatsXml.read(f, stats);
        if (beginTime < stats.endTime) {
            combiner.combine(stats, false, results);
        }
    }
    return results;
}
  • 第三部分是將內存和磁盤的數(shù)據(jù)合并起來。

到這里我們就知道,取數(shù)據(jù)的時候是內存和磁盤數(shù)據(jù)的合集,那么究竟該怎么取數(shù)據(jù)才能比較準確呢?看看系統(tǒng)怎么存儲數(shù)據(jù)的。

3.4 系統(tǒng)存數(shù)據(jù)源碼追蹤

記得UsageStatsService在系統(tǒng)初始化的時候,會將他的一個對象設置到AMS,這里就是數(shù)據(jù)存儲被觸發(fā)的地方:

ActivityManagerService

void updateUsageStats(ActivityRecord component, boolean resumed) {
    if (resumed) {
        if (mUsageStatsService != null) {
            mUsageStatsService.reportEvent(component.realActivity, component.userId, UsageEvents.Event.MOVE_TO_FOREGROUND);
        }
    } else {
        if (mUsageStatsService != null) {
            mUsageStatsService.reportEvent(component.realActivity, component.userId, UsageEvents.Event.MOVE_TO_BACKGROUND);
        }
    }
}

updateUsageStats方法在三個地方被調用:

  • ActivityStackSupervisor,reportResumedActivityLocked
  • ActivityStack,startPausingLocked
  • ActivityStack,removeHistoryRecordsForAppLocked

從這三個方法名稱可以看出來一般都是Activity從前臺切換到后臺,或從后臺到前臺時會觸發(fā)這個方法。

從updateUsageStats方法中可以看出,分為MOVE_TO_FOREGROUND,MOVE_TO_BACKGROUND調用reportEvent方法。

這里的mUsageStatsServiceUsageStatsManagerInternal類型,記得在UsageStatsService的start方法中有publishLocalService(UsageStatsManagerInternal.class, new LocalService());方法,這里UsageStatsManagerInternal是type,LocalService是type對應的service,而LocalService繼承自UsageStatsManagerInternal,因此這里具體操作在UsageStatsService的內部類LocalService中。

UsageStatsService.LocalService

private final class BinderService extends IUsageStatsManager.Stub {
    @Override
    public void reportEvent(ComponentName component, int userId, int eventType) {
        UsageEvents.Event event = new UsageEvents.Event();
        event.mPackage = component.getPackageName();
        event.mClass = component.getClassName();

        // This will later be converted to system time.
        event.mTimeStamp = SystemClock.elapsedRealtime();

        event.mEventType = eventType;
        mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
    }
}

這里新建一個UsageEvents.Event對象,將包名,組件名,時間,類型填充起來,通過UsageStatsService onStart方法中初始化的mHandler中串行的處理消息:

UsageStatsService

class H extends Handler {
    public H(Looper looper) {
        super(looper);
    }
    
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_REPORT_EVENT:
                reportEvent((UsageEvents.Event) msg.obj, msg.arg1);
                break;
            
            case MSG_FLUSH_TO_DISK:
                flushToDisk();
                break;
        }
    }
}

調用外部類的reportEvent方法:

UsageStatsService

void reportEvent(UsageEvents.Event event, int userId) {
    final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
    service.reportEvent(event);
}

UserUsageStatsService

void reportEvent(UsageEvents.Event event) {
    final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
    
    // Add the event to the daily list.
    if (currentDailyStats.events == null) {
        currentDailyStats.events = new EventList();
    }
    if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) {
        currentDailyStats.events.insert(event);
    }
    
    for (IntervalStats stats : mCurrentStats) {
        switch (event.mEventType) {
            default: {
                stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
                break;
            }
        }
    }
    notifyStatsChanged();
}
  • 第一步先把數(shù)據(jù)放到Daily所屬的文件中,也就是放到內存中。
  • 第二步,調用IntervalStats的update方法實施更新??纯催@里一連串的操作:

IntervalStats

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

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

    endTime = timeStamp;
}

UsageStats getOrCreateUsageStats(String packageName) {
    UsageStats usageStats = packageStats.get(packageName);
    if (usageStats == null) {
        usageStats = new UsageStats();
        usageStats.mPackageName = getCachedStringRef(packageName);
        usageStats.mBeginTimeStamp = beginTime;
        usageStats.mEndTimeStamp = endTime;
        packageStats.put(usageStats.mPackageName, usageStats);
    }
    return usageStats;
}

到這里可以知道每一種時間類型對應的IntervalStats對象里面維持一個UsageStats對象,這個對象里面包含了包名,開始使用時間,結束使用時間數(shù)據(jù)。

數(shù)據(jù)都準備好了,接下來調用notifyStatsChanged

UserUsageStatsService

private void notifyStatsChanged() {
    if (!mStatsChanged) {
        mStatsChanged = true;
        mListener.onStatsUpdated();
    }
}

而這里的mListener是UsageStatsService傳遞過來的,對應的onStatsUpdated在這個類中實現(xiàn):

UsageStatsService

private static final long TEN_SECONDS = 10 * 1000;
private static final long TWENTY_MINUTES = 20 * 60 * 1000;
private static final long FLUSH_INTERVAL = COMPRESS_TIME ? TEN_SECONDS : TWENTY_MINUTES;

@Override
public void onStatsUpdated() {
    mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
}

還是在這個H類中處理,這里的FLUSH_INTERVAL是20分鐘,也就是要間隔這么長時間才去寫數(shù)據(jù)到磁盤:

UsageStatsService.H

class H extends Handler {
    public H(Looper looper) {
        super(looper);
    }
    
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_FLUSH_TO_DISK:
                flushToDisk();
                break;
        }
    }
}

UsageStatsService

void flushToDisk() {
    synchronized (mLock) {
        flushToDiskLocked();
    }
}

private void flushToDiskLocked() {
    final int userCount = mUserState.size();
    for (int i = 0; i < userCount; i++) {
        UserUsageStatsService service = mUserState.valueAt(i);
        service.persistActiveStats();
    }
    mHandler.removeMessages(MSG_FLUSH_TO_DISK);
}

還是到UserUsageStatsService類中處理,而且有多個用戶的話,為多個用戶分別存儲數(shù)據(jù):

UserUsageStatsService

void persistActiveStats() {
    if (mStatsChanged) {
        try {
            for (int i = 0; i < mCurrentStats.length; i++) {
                mDatabase.putUsageStats(i, mCurrentStats[i]);
            }
            mStatsChanged = false;
        } catch (IOException e) {
        }
    }
}

這里將為各個時間間隔類型的文件中都寫入數(shù)據(jù)。接下來在UsageStatsDatabase中調用putUsageStats方法:

UsageStatsDatabase

public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
    synchronized (mLock) {
        //第一部分
        AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
        if (f == null) {
            f = new AtomicFile(new File(mIntervalDirs[intervalType],
            Long.toString(stats.beginTime)));
            mSortedStatFiles[intervalType].put(stats.beginTime, f);
        }
        
        //第二部分
        UsageStatsXml.write(f, stats);
        stats.lastTimeSaved = f.getLastModifiedTime();
    }
}

TimeSparseArray<AtomicFile>[] mSortedStatFiles,繼承自LongSpareArray

  • 第一部分中,先獲取mSortedStatFiles中對應時間的文件是否存在,不存在的話就按照對應的時間間隔類型新建一個,創(chuàng)建完成之后將時間作為key,文件對象作為value添加到TimeSparseArray集合中。這個類型是有序的,而且會先通過二分查找這個key,如果存在,就要覆寫數(shù)據(jù)了。

  • 第二部分通過調用UsageStatsXml.write方法執(zhí)行寫xml操作:

UsageStatsXml

private static final String USAGESTATS_TAG = "usagestats";

static void write(OutputStream out, IntervalStats stats) throws IOException {
    FastXmlSerializer xml = new FastXmlSerializer();
    xml.setOutput(out, "utf-8");
    xml.startDocument("utf-8", true);
    xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    xml.startTag(null, USAGESTATS_TAG);
    xml.attribute(null, VERSION_ATTR, Integer.toString(CURRENT_VERSION));

    UsageStatsXmlV1.write(xml, stats);

    xml.endTag(null, USAGESTATS_TAG);
    xml.endDocument();
}

開始標簽是USAGESTATS_TAG,通過UsageStatsXmlV1寫數(shù)據(jù):

UsageStatsXmlV1

public static void write(XmlSerializer xml, IntervalStats stats) throws IOException {
    xml.startTag(null, PACKAGES_TAG);
    final int statsCount = stats.packageStats.size();
    for (int i = 0; i < statsCount; i++) {
        writeUsageStats(xml, stats, stats.packageStats.valueAt(i));
    }
    xml.endTag(null, PACKAGES_TAG);
}

private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats, final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);

    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR, usageStats.mLastTimeUsed - stats.beginTime);

    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    if (usageStats.mAppLaunchCount > 0) {
        XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
    }
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}

到這里我們發(fā)現(xiàn)包名,時長,使用次數(shù),mLastEvent都被寫入磁盤了。

mLastEvent對應的是前臺或后臺事件,是int類型,前臺為1,后臺為2,一天的結束時間事件為3。

3.5 項目問題復盤

  • 結合源碼分析問題

Android9.0以后將應用使用詳情的大多數(shù)數(shù)據(jù)都寫到磁盤了,但是Android 9.0以下的版本中沒有將應用使用次數(shù)寫到磁盤。另外還要面臨延遲20分鐘寫磁盤的操作,如果每次都從磁盤取數(shù)據(jù),在Android 9.0以下的版本中讀取的的次數(shù)一定是不準確的。

相關的版本差異如下:

//Android 7.1
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    xml.endTag(null, PACKAGE_TAG);
}
//Android 8.1
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}

//Android 9.0
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    if (usageStats.mAppLaunchCount > 0) {
        XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
    }
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}
//Android 10.0
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeLongAttribute(xml, LAST_TIME_VISIBLE_ATTR,
            usageStats.mLastTimeVisible - stats.beginTime);
    XmlUtils.writeLongAttribute(xml, LAST_TIME_SERVICE_USED_ATTR,
            usageStats.mLastTimeForegroundServiceUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_VISIBLE_ATTR, usageStats.mTotalTimeVisible);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_SERVICE_USED_ATTR,
            usageStats.mTotalTimeForegroundServiceUsed);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    if (usageStats.mAppLaunchCount > 0) {
        XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
    }
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}

到后面,寫入的數(shù)據(jù)顆粒度越來越小,比如應用可見時長,前臺服務的時長等都被寫入磁盤。這是因為后面Android在設置中也做了應用使用詳情功能,如果這些數(shù)據(jù)不寫入的話,數(shù)據(jù)會有出入。

3.5.1 問題解決方案

項目早期,這個App屬于系統(tǒng)級別的App,我們可以通過監(jiān)聽滅屏廣播,在滅屏之后立即獲取上一次滅屏到此次滅屏時間段內的應用使用數(shù)據(jù),雖然這段時間間隔會大于20分鐘,但是滅屏之后,最新的數(shù)據(jù)會先被寫入內存,而之前的數(shù)據(jù)在大于20分鐘會被寫入磁盤導致一部分次數(shù)的數(shù)據(jù)丟失,但是出現(xiàn)的概率比較低,可以接受。

到項目后期,App的系統(tǒng)級別屬性被去掉,只能作為一個普通App開發(fā)了,這里一方面修改framework,將應用使用次數(shù)也持久化到磁盤;如果framework的這個patch沒有集成的話,可以在另外一個系統(tǒng)級服務中實現(xiàn)之前早期項目App的那一套保存數(shù)據(jù)邏輯,將數(shù)據(jù)即時存到本地數(shù)據(jù)庫,并對外提供數(shù)據(jù)接口,同時加強權限判斷,避免被亂用。這樣App就可以獲取到最新的數(shù)據(jù)了。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容