我的主頁:chengang.plus/html
文章將會同步到我的微信公衆號:Android部落格java
這個項目遇到的主要問題是應用使用時長和使用次數不許確的問題。緣由要從應用的業務邏輯以及源碼中去查找。linux
通常咱們獲取應用使用數據詳情的方法是:數據庫
@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);
}
複製代碼
當每次打開應用的時候,經過上述方法去取使用數據,或者每次從應用其餘頁面回到首頁的時候去取,將取到的數據持久化保存到本地數據庫。數組
這種使用方式看起來很合理,可是測試人員老是反饋應用使用時長和次數不許確。到這裏就須要從源碼找緣由了。bash
咱們都知道linux從init.rc腳本啓動了Zygote,Zygote 經過fork建立了system_server進程,這個集成所屬的類是SystemServer
,在他的run方法中啓動了一些列的系統服務,咱們重點關注UsageStatsService
什麼時候啓動。微信
SystemServeride
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調度。函數
SystemServiceManageroop
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
,這個是爲多用戶的狀況下處理數據準備的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方法,這個方法的目的是讀取已有的數據,沒有相關的數據就初始化建立。
到這裏基本的初始化工做就完成了。
客戶端的調用代碼是:
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
@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;
}
複製代碼
mCurrentStats是IntervalStats數組類型,而IntervalStats裏面維護了一個EventList對象,這個對象裏面持有一個ArrayList<UsageEvents.Event> mEvents,維護應用使用詳情數據。
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;
}
複製代碼
到這裏咱們就知道,取數據的時候是內存和磁盤數據的合集,那麼究竟該怎麼取數據才能比較準確呢?看看系統怎麼存儲數據的。
記得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
方法在三個地方被調用:
從這三個方法名稱能夠看出來通常都是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();
}
複製代碼
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。
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在設置中也作了應用使用詳情功能,若是這些數據不寫入的話,數據會有出入。
項目早期,這個App屬於系統級別的App,咱們能夠經過監聽滅屏廣播,在滅屏以後當即獲取上一次滅屏到這次滅屏時間段內的應用使用數據,雖然這段時間間隔會大於20分鐘,可是滅屏以後,最新的數據會先被寫入內存,而以前的數據在大於20分鐘會被寫入磁盤致使一部分次數的數據丟失,可是出現的機率比較低,能夠接受。
到項目後期,App的系統級別屬性被去掉,只能做爲一個普通App開發了,這裏一方面修改framework,將應用使用次數也持久化到磁盤;若是framework的這個patch沒有集成的話,能夠在另一個系統級服務中實現以前早期項目App的那一套保存數據邏輯,將數據即時存到本地數據庫,並對外提供數據接口,同時增強權限判斷,避免被亂用。這樣App就能夠獲取到最新的數據了。