Android識別預(yù)裝的第三方App方法實(shí)例
前言
新買一臺手機(jī),里面會有很多App,有的屬于系統(tǒng)App,不可卸載,有的屬于第三方App,廠商會預(yù)裝一些常用的或者給了他們廣告費(fèi)的App,這些是可以卸載的。
如果要詳細(xì)劃分,系統(tǒng)App還可根據(jù)其路徑不同進(jìn)一步劃分(如/system/app、/system/priv-app、/vendor/app等)。但對于開發(fā)者來說,手機(jī)上安裝的App只分為2類:系統(tǒng)App和用戶App,可以根據(jù)系統(tǒng)API區(qū)分,這里就不詳細(xì)說了,簡單而言存在ApplicationInfo.FLAG_SYSTEM或ApplicationInfo.FLAG_UPDATED_SYSTEM_APP flag的即為系統(tǒng)App,否則為用戶App。
但是,利用這個(gè)方法那些預(yù)裝的App也會歸到用戶App中,那么有沒有辦法知道用戶App中哪些是預(yù)裝的哪些是用戶手動安裝的呢?
在這里分享一種方法:App的安裝時(shí)間是整秒的為預(yù)裝的第三方App。
如果不關(guān)心為什么能用這個(gè)奇怪方法來區(qū)分預(yù)裝App的話,就可以關(guān)閉這篇文章了。
之前我也一直不清楚為什么可以用這種方法,當(dāng)時(shí)我猜是因?yàn)槭謾C(jī)第一次啟動的時(shí)候時(shí)間是不準(zhǔn)確的,會是某某年1月1日,然后因?yàn)閱訒r(shí)會掃描各個(gè)App目錄然后安裝App,因此被打上這樣的安裝時(shí)間。但這種解釋是說不通的,即便真是這樣,那安裝時(shí)間也不應(yīng)該是整秒的。因此我決定好好找一下原因,以下是我求證的歷程。
背景知識
首先介紹一些背景知識。
App的安裝時(shí)間可以通過獲取PackageInfo得到,其firstInstallTime屬性即安裝時(shí)間。
/data/system/packages.xml保存了手機(jī)上安裝的App的信息,其中App的安裝時(shí)間就保存在這里。我在下面截取了2個(gè)示例。
<package name="com.tencent.mm" codePath="/data/app/com.tencent.mm-TSn6yG4fF7A_EaxE5OtrHQ==" nativeLibraryPath="/data/app/com.tencent.mm-TSn6yG4fF7A_EaxE5OtrHQ==/lib" primaryCpuAbi="armeabi" publicFlags="945307204" privateFlags="0" ft="167702c7508" it="1676feab448" ut="167702c8a57" version="1360" userId="10118"> ... </package> <package name="com.android.providers.downloads" codePath="/system/priv-app/DownloadProvider" nativeLibraryPath="/system/priv-app/DownloadProvider/lib" publicFlags="944258629" privateFlags="8" ft="11e8dc5d800" it="11e8dc5d800" ut="11e8dc5d800" version="28" sharedUserId="10006" isOrphaned="true"> ... </package>
其中it的值便是安裝時(shí)間,這里是用十六進(jìn)制保存的,上面這2個(gè)App的安裝時(shí)間換算成十進(jìn)制分別是1543770911816和1230739200000,對應(yīng)的北京時(shí)間即2018-12-03 01:15:11和2009-01-01 00:00:00。 【嗯…想不到我12月3號1點(diǎn)多還沒睡,還安裝了個(gè)微信……】
系統(tǒng)啟動時(shí),PackageManagerService由SystemServer啟動,PackageManagerService會掃描/data/app、/system/app、/system/priv-app、/vendor/app等等目錄,可以理解為會把這些目錄中的Apk安裝一遍,PackageManagerService會結(jié)合上面提到的packages.xml把各個(gè)App解析成PackageParser.Package對象。
思路
根據(jù)上面的知識,我們可以知道,如果packages.xml已經(jīng)有了某個(gè)App的信息,那么這個(gè)App的安裝時(shí)間肯定就是packages.xml中記錄的時(shí)間。第一次啟動手機(jī)時(shí)packages.xml文件還不存在,或者新安裝一個(gè)App時(shí),packages.xml中還沒有這個(gè)App的記錄,也就是說,確認(rèn)這個(gè)packages.xml中的firstInstallTime(即it)是如果生成的便是問題的關(guān)鍵。
以下基于7.0.0_r1版本代碼。
通過搜索PackageManagerService,在scanPackageDirtyLI方法中有這么一段代碼:
// Take care of first install / last update times.
if (currentTime != 0) {
if (pkgSetting.firstInstallTime == 0) {
pkgSetting.firstInstallTime = pkgSetting.lastUpdateTime = currentTime;
} else if ((scanFlags&SCAN_UPDATE_TIME) != 0) {
pkgSetting.lastUpdateTime = currentTime;
}
} else if (pkgSetting.firstInstallTime == 0) {
// We need *something*. Take time time stamp of the file.
pkgSetting.firstInstallTime = pkgSetting.lastUpdateTime = scanFileTime;
} else if ((policyFlags&PackageParser.PARSE_IS_SYSTEM_DIR) != 0) {
if (scanFileTime != pkgSetting.timeStamp) {
// A package on the system image has changed; consider this
// to be an update.
pkgSetting.lastUpdateTime = scanFileTime;
}
}
其中,currentTime是scanPackageDirtyLI方法的一個(gè)參數(shù)。pkgSetting是從packages.xml中讀取到的該App的信息(PackageSetting對象),如果packages.xml中不存在這個(gè)App的信息,會根據(jù)從Apk中解析到的信息創(chuàng)建一個(gè)PackageSetting。scanFileTime是Apk文件的最后修改時(shí)間。
可以看到存在這么幾種情況:
- 傳入的currentTime不為0,從packages.xml中讀取到的firstInstallTime為0。這種情況會將firstInstallTime和lastUpdateTime均設(shè)置為傳入的currentTime的值。
- 傳入的currentTime不為0,傳入的scanFlags設(shè)置了SCAN_UPDATE_TIME。這種情況會將lastUpdateTime設(shè)置為傳入的currentTime的值。
- 傳入的currentTime為0,從packages.xml中讀取到的firstInstallTime為0。這種情況會將firstInstallTime和lastUpdateTime均設(shè)置為Apk的最后修改時(shí)間。
- 傳入的currentTime為0,從packages.xml中讀取到的firstInstallTime不為0,傳入的policyFlags設(shè)置了PackageParser.PARSE_IS_SYSTEM_DIR,scanFileTime與packages.xml中讀取到的timeStamp(packages.xml中package標(biāo)簽的ft)不相同。這種情況會將lastUpdateTime設(shè)置為Apk的最后修改時(shí)間。
對應(yīng)到我們真實(shí)使用手機(jī)的場景,上面4種情況分別對應(yīng)以下幾種場景:
- 第一種情況:對應(yīng)新安裝App。currentTime為當(dāng)前的時(shí)間戳,會將這個(gè)新安裝的App的firstInstallTime和lastUpdateTime設(shè)置為當(dāng)前時(shí)間戳。
- 第二種情況:對應(yīng)更新App。currentTime為當(dāng)前的時(shí)間戳,會將lastUpdateTime設(shè)置為當(dāng)前時(shí)間戳,firstInstallTime保持不變。
- 第三種情況:手機(jī)啟動時(shí)PackageManagerService掃描各個(gè)目錄時(shí)發(fā)現(xiàn)了packages.xml中不存在的App(第一次啟動時(shí)所有App都不在packages.xml中)。
- 第四種情況:系統(tǒng)更新等操作更新了系統(tǒng)分區(qū)的App,導(dǎo)致其文件的最后修改時(shí)間和記錄的不一致了,會被認(rèn)為是更新。
我們可以大膽猜測,第一次啟動手機(jī)時(shí)會走第三種情況,因此系統(tǒng)App和預(yù)裝App的安裝時(shí)間是文件的最后修改時(shí)間,而這些文件的最后修改時(shí)間都是整秒的。
如何驗(yàn)證?
我們先看看上面那個(gè)com.android.providers.downloads的Apk文件的最后修改時(shí)間。
# stat DownloadProvider.apk File: `DownloadProvider.apk' Size: 504712 Blocks: 992 IO Blocks: 512 regular file Device: 10305h/66309d Inode: 1308 Links: 1 Access: (644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2009-01-01 00:00:00.000000000 Modify: 2009-01-01 00:00:00.000000000 Change: 2009-01-01 00:00:00.000000000
時(shí)間與packages.xml中保存的時(shí)間一致,確實(shí)是把文件的最后修改時(shí)間作為了安裝時(shí)間。那么還有一個(gè)問題需要確認(rèn),傳入的currentTime是0嗎?
我們追溯調(diào)用鏈,會在PackageManagerService的構(gòu)造函數(shù)中看到掃描各個(gè)目錄的方法。調(diào)用scanDirTracedLI方法傳入的最后一個(gè)參數(shù)0即scanPackageDirtyLI方法中的currentTime。感興趣的還可以仔細(xì)看看PackageManagerService到底掃描了哪些目錄。
File vendorOverlayDir = new File(VENDOR_OVERLAY_DIR);
scanDirTracedLI(vendorOverlayDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_TRUSTED_OVERLAY, scanFlags | SCAN_TRUSTED_OVERLAY, 0);
// Find base frameworks (resource packages without code).
scanDirTracedLI(frameworkDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED,
scanFlags | SCAN_NO_DEX, 0);
// Collected privileged system packages.
final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
scanDirTracedLI(privilegedAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0);
// Collect ordinary system packages.
final File systemAppDir = new File(Environment.getRootDirectory(), "app");
scanDirTracedLI(systemAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);
// Collect all vendor packages.
File vendorAppDir = new File("/vendor/app");
try {
vendorAppDir = vendorAppDir.getCanonicalFile();
} catch (IOException e) {
// failed to look up canonical path, continue with original one
}
scanDirTracedLI(vendorAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);
// Collect all OEM packages.
final File oemAppDir = new File(Environment.getOemDirectory(), "app");
scanDirTracedLI(oemAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);
...
scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
scanDirTracedLI(mDrmAppPrivateInstallDir, mDefParseFlags
| PackageParser.PARSE_FORWARD_LOCK,
scanFlags | SCAN_REQUIRE_KNOWN, 0);
如果感興趣,你可以去跟一下安裝App和更新App的代碼,看傳入的currentTime是不是當(dāng)前的時(shí)間戳。
到此,我們已經(jīng)證明了第一次啟動手機(jī)時(shí),系統(tǒng)會把文件的最后修改時(shí)間當(dāng)成系統(tǒng)App和預(yù)裝App的安裝時(shí)間,而這個(gè)時(shí)間一般是類似于上面那樣2009-01-01 00:00:00.000000000的整秒的時(shí)間(至于為什么是這樣,那就是另一個(gè)問題了),而我們自己安裝App時(shí)幾乎不可能在一個(gè)整秒的時(shí)間安裝,所有我們可以用安裝時(shí)間是否為整秒來區(qū)分手機(jī)預(yù)裝的App和用戶手動安裝的App。
至于區(qū)分預(yù)裝App和用戶手動安裝的App有什么用?請發(fā)揮你的想象,比如說,一個(gè)用戶的手機(jī)上只有你家一個(gè)手動安裝的App或者少數(shù)幾個(gè)App,那么他是想干什么好事呢?
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Android中隱藏狀態(tài)欄和標(biāo)題欄的方法匯總(隱藏狀態(tài)欄、標(biāo)題欄的五種方法)
這篇文章主要介紹了Android中隱藏狀態(tài)欄和標(biāo)題欄的方法匯總(隱藏狀態(tài)欄、標(biāo)題欄的五種方法),非常不錯,具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
Android 系統(tǒng)簽名 keytool-importkeypair的操作步驟
本文介紹了在Android項(xiàng)目中使用系統(tǒng)簽名并打包APK的方法,步驟包括獲取系統(tǒng)簽名文件、修改AndroidManifest.xml、轉(zhuǎn)換簽名文件、配置Android Studio簽名、生成APK文件和打包APK,感興趣的朋友一起看看吧2025-01-01
Android判斷json格式將錯誤信息提交給服務(wù)器
今天小編就為大家分享一篇關(guān)于Android判斷json格式將錯誤信息提交給服務(wù)器,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03
Flutter進(jìn)階之實(shí)現(xiàn)動畫效果(九)
這篇文章主要為大家詳細(xì)介紹了Flutter進(jìn)階之實(shí)現(xiàn)動畫效果的第九篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
模仿美團(tuán)點(diǎn)評的Android應(yīng)用中價(jià)格和購買欄懸浮固定的效果
這篇文章主要介紹了模仿美團(tuán)點(diǎn)評的Android應(yīng)用中價(jià)格和購買欄懸浮固定的效果,文章后半還針對快速滑動操作給出了一個(gè)響應(yīng)優(yōu)化的方法,需要的朋友可以參考下2016-04-04
android實(shí)現(xiàn)直播點(diǎn)贊飄心動畫效果
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)直播點(diǎn)贊飄心動畫效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
Android編程程序?qū)崿F(xiàn)一鍵鎖屏的方法講解
今天小編就為大家分享一篇關(guān)于Android編程程序?qū)崿F(xiàn)一鍵鎖屏的方法講解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03

