Android项目复盘3
个人主页:https://chengang.plus/
文章将会同步到个人微信公众号: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方法比较长,只提取比较重要的方法@Overridepublic 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;}
这里的mService
是IUsageStatsManager
类型,是服务端的操作对象,对应的是服务端UsageStatsService
的内部类BinderService
,也就是对应的调用其中的方法:
UsageStatsService.BinderService
@Overridepublic 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方法。
这里的mUsageStatsService
是UsageStatsManagerInternal
类型,记得在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;@Overridepublic 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.1private 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.1private 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.0private 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.0private 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就可以获取到最新的数据了。
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- 一句话锁定MySQL数据占用元凶
- Android中BaseAdapter的用法分析与理解
- android中的三种适配器
- Android:Umeng(友盟)数据统计(一)
- Android中View跟随手指滑动效果的实例代码
- runOnUiThread()方法
- 【Android高级工程师】Android项目开发如何设计整体架构?
- Android欢迎界面的创建方法