Android项目总结复盘3

文章将会同步到个人微信公众号:Android部落格

3、健康数据记录项目

这个项目遇到的主要问题是应用使用时长和使用次数不准确的问题。原因要从应用的业务逻辑以及源码中去查找。

一般我们获取应用使用数据详情的方法是:

@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 业务逻辑

当每次打开应用的时候,通过上述方法去取使用数据,或者每次从应用其他页面回到首页的时候去取,将取到的数据持久化保存到本地数据库。

这种使用方式看起来很合理,但是测试人员总是反馈应用使用时长和次数不准确。到这里就需要从源码找原因了。

3.2 UsageStatsManager源码追溯

我们都知道linux从init.rc脚本启动了Zygote,Zygote 通过fork创建了system_server进程,这个集成所属的类是SystemServer,在他的run方法中启动了一些列的系统服务,我们重点关注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统一管理系统服务,交给它去启动服务,并且将启动之后的服务交给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的构造函数,构造完成之后通过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的构造函数比较简单,重点分析start方法:

  • 第一部分中获取了UserManager,这个是为多用户的情况下处理数据准备的
  • 第二部分中创建了一个Handler,用于处理数据,比如存储数据到磁盘,他的looper其实来自于HandlerThread,因为BackgroundThread继承自HandlerThread。
  • 第三部分是在/data/system/目录下创建一个usagestats的文件夹,用于创建文件存放数据。
  • 第四部分中,其实是将LocalService对象添加到LocalServices的集合中,而LocalService是UsageStatsService的内部类;publishBinderService做的事情就是将BinderService添加到ServiceManager中。BinderService的定义是:
private final class BinderService extends IUsageStatsManager.Stub {}

我们知道IUsageStatsManager.Stub是对客户端提供的代理对象,客户端获取到对象进行对应的操作,而具体的操作函数就定义在BinderService覆写的方法中。

  • 第五部分意在初始化一个UserUsageStatsService类,在初始化的时候回传递userId,根据这个userId创建对应的文件夹存储不同用户的数据:

UserUsageStatsService

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

UserUsageStatsService构造函数中又创建了一个UsageStatsDatabase对象,以及IntervalStats类型的数组。

前者主要功能是往xml文件中写数据,后者的主要功能是处理不同时间间隔的数据。

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

这里又分不同的时间属性创建文件夹存放数据。

UserUsageStatsService最后会调用init方法,这个方法的目的是读取已有的数据,没有相关的数据就初始化创建。

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

3.3 客户端获取数据源码追踪

客户端的调用代码是:

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的数据被report的时候,一开始是放到mCurrentStats里面存起来。

    mCurrentStats是IntervalStats数组类型,而IntervalStats里面维护了一个EventList对象,这个对象里面持有一个ArrayList mEvents,维护应用使用详情数据。

  • 第二部分从本地磁盘的xml文件取需要的时间间隔内的数据。在取到数据之后回调combine方法将数据存放到一个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;
}
  • 第三部分是将内存和磁盘的数据合并起来。

到这里我们就知道,取数据的时候是内存和磁盘数据的合集,那么究竟该怎么取数据才能比较准确呢?看看系统怎么存储数据的。

3.4 系统存数据源码追踪

记得UsageStatsService在系统初始化的时候,会将他的一个对象设置到AMS,这里就是数据存储被触发的地方:

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从前台切换到后台,或从后台到前台时会触发这个方法。

从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();
}
  • 第一步先把数据放到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对象,这个对象里面包含了包名,开始使用时间,结束使用时间数据。

数据都准备好了,接下来调用notifyStatsChanged

UserUsageStatsService

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

而这里的mListener是UsageStatsService传递过来的,对应的onStatsUpdated在这个类中实现:

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分钟,也就是要间隔这么长时间才去写数据到磁盘:

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类中处理,而且有多个用户的话,为多个用户分别存储数据:

UserUsageStatsService

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

这里将为各个时间间隔类型的文件中都写入数据。接下来在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[] mSortedStatFiles,继承自LongSpareArray

  • 第一部分中,先获取mSortedStatFiles中对应时间的文件是否存在,不存在的话就按照对应的时间间隔类型新建一个,创建完成之后将时间作为key,文件对象作为value添加到TimeSparseArray集合中。这个类型是有序的,而且会先通过二分查找这个key,如果存在,就要覆写数据了。

  • 第二部分通过调用UsageStatsXml.write方法执行写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写数据:

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

到这里我们发现包名,时长,使用次数,mLastEvent都被写入磁盘了。

mLastEvent对应的是前台或后台事件,是int类型,前台为1,后台为2,一天的结束时间事件为3。

3.5 项目问题复盘

  • 结合源码分析问题

Android9.0以后将应用使用详情的大多数数据都写到磁盘了,但是Android 9.0以下的版本中没有将应用使用次数写到磁盘。另外还要面临延迟20分钟写磁盘的操作,如果每次都从磁盘取数据,在Android 9.0以下的版本中读取的的次数一定是不准确的。

相关的版本差异如下:

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

到后面,写入的数据颗粒度越来越小,比如应用可见时长,前台服务的时长等都被写入磁盘。这是因为后面Android在设置中也做了应用使用详情功能,如果这些数据不写入的话,数据会有出入。

3.5.1 问题解决方案

项目早期,这个App属于系统级别的App,我们可以通过监听灭屏广播,在灭屏之后立即获取上一次灭屏到此次灭屏时间段内的应用使用数据,虽然这段时间间隔会大于20分钟,但是灭屏之后,最新的数据会先被写入内存,而之前的数据在大于20分钟会被写入磁盘导致一部分次数的数据丢失,但是出现的概率比较低,可以接受。

到项目后期,App的系统级别属性被去掉,只能作为一个普通App开发了,这里一方面修改framework,将应用使用次数也持久化到磁盘;如果framework的这个patch没有集成的话,可以在另外一个系统级服务中实现之前早期项目App的那一套保存数据逻辑,将数据即时存到本地数据库,并对外提供数据接口,同时加强权限判断,避免被乱用。这样App就可以获取到最新的数据了。