新聞中心
前言

10年積累的網(wǎng)站制作、成都網(wǎng)站制作經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先建設(shè)網(wǎng)站后付款的網(wǎng)站建設(shè)流程,更有蘆淞免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
大家應(yīng)該都熟悉FileProvider吧,但是其誕生的原因,內(nèi)部怎么實現(xiàn)的,又是怎么轉(zhuǎn)化為文件的,大家有了解多少呢?今天就通過它重新看看ContentProvider這個四大組件之一。
在Android7.0,Android提高了應(yīng)用的隱私權(quán),限制了在應(yīng)用間共享文件。如果需要在應(yīng)用間共享,需要授予要訪問的URI臨時訪問權(quán)限。
以下是官方說明:
對于面向 Android 7.0 的應(yīng)用,Android 框架執(zhí)行的 StrictMode API 政策禁止在您的應(yīng)用外部公開 file:// URI。如果一項包含文件 URI 的 intent 離開您的應(yīng)用,則應(yīng)用出現(xiàn)故障,并出現(xiàn) FileUriExposedException 異常。要在應(yīng)用間共享文件,您應(yīng)發(fā)送一項 content:// URI,并授予 URI 臨時訪問權(quán)限。進(jìn)行此授權(quán)的最簡單方式是使用 FileProvider 類?!?/p>
為什么限制在應(yīng)用間共享文件
打個比方,應(yīng)用A有一個文件,絕對路徑為file:///storage/emulated/0/Download/photo.jpg
現(xiàn)在應(yīng)用A想通過其他應(yīng)用來完成一些需求,比如拍照,就把他的這個文件路徑發(fā)給了照相應(yīng)用B,然后應(yīng)用B照完相就把照片存儲到了這個絕對路徑。
看起來似乎沒有什么問題,但是如果這個應(yīng)用B是個“壞應(yīng)用”呢?
- 泄漏了文件路徑,也就是應(yīng)用隱私。
如果這個應(yīng)用A是“壞應(yīng)用”呢?
- 自己可以不用申請存儲權(quán)限,利用應(yīng)用B就達(dá)到了存儲文件的這一危險權(quán)限。
可以看到,這個之前落伍的方案,從自身到對方,都是不太好的選擇。
所以Google就想了一個辦法,把對文件的訪問限制在應(yīng)用內(nèi)部。
- 如果要分享文件路徑,不要分享file:// URI這種文件的絕對路徑,而是分享content:// URI,這種相對路徑,也就是這種格式:content://com.jimu.test.fileprovider/external/photo.jpg
- 然后其他應(yīng)用可以通過這個絕對路徑來向文件所屬應(yīng)用 索要 文件數(shù)據(jù),所以文件所屬的應(yīng)用本身必須擁有文件的訪問權(quán)限。
也就是應(yīng)用A分享相對路徑給應(yīng)用B,應(yīng)用B拿著這個相對路徑找到應(yīng)用A,應(yīng)用A讀取文件內(nèi)容返給應(yīng)用B。
配置FileProvider
搞清楚了要做什么事,接下來就是怎么做。
涉及到應(yīng)用間通信的問題,還記得IPC的幾種方式嗎?
- 文件
- AIDL
- ContentProvider
- Socket
- 等等。
從易用性,安全性,完整度等各個方面考慮,Google選擇了ContentProvider為這次限制應(yīng)用分享文件的 解決方案。于是,F(xiàn)ileProvider誕生了。
具體做法就是:
- android:name="androidx.core.content.FileProvider"
- android:authorities="${applicationId}.provider"
- android:exported="false"
- android:grantUriPermissions="true">
- android:name="android.support.FILE_PROVIDER_PATHS"
- android:resource="@xml/provider_paths"/>
- //修改文件URL獲取方式
- Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", createImageFile());
這樣配置之后,就能生成content:// URI,并且也能通過這個URI來傳輸文件內(nèi)容給外部應(yīng)用。
FileProvider這些配置屬性也就是ContentProvider的通用配置:
- android:name,是ContentProvider的類路徑。
- android:authorities,是唯一標(biāo)示,一般為包名+.provider
- android:exported,表示該組件是否能被其他應(yīng)用使用。
- android:grantUriPermissions,表示是否允許授權(quán)文件的臨時訪問權(quán)限。
其中要注意的是android:exported正常應(yīng)該是true,因為要給外部應(yīng)用使用。
但是FileProvider這里設(shè)置為false,并且必須為false。
這主要為了保護(hù)應(yīng)用隱私,如果設(shè)置為true,那么任何一個應(yīng)用都可以來訪問當(dāng)前應(yīng)用的FileProvider了,對于應(yīng)用文件來說不是很可取,所以Android7.0以上會通過其他方式讓外部應(yīng)用安全的訪問到這個文件,而不是普通的ContentProvider訪問方式,后面會說到。
也正是因為這個屬性為true,在Android7.0以下,Android默認(rèn)是將它當(dāng)成一個普通的ContentProvider,外部無法通過content:// URI來訪問文件。所以一般要判斷下系統(tǒng)版本再確定傳入的Uri到底是File格式還是content格式。
FileProvider源碼
接著看看FileProvider的主要源碼:
- public class FileProvider extends ContentProvider {
- @Override
- public boolean onCreate() {
- return true;
- }
- @Override
- public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
- super.attachInfo(context, info);
- // Sanity check our security
- if (info.exported) {
- throw new SecurityException("Provider must not be exported");
- }
- if (!info.grantUriPermissions) {
- throw new SecurityException("Provider must grant uri permissions");
- }
- mStrategy = getPathStrategy(context, info.authority);
- }
- public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
- @NonNull File file) {
- final PathStrategy strategy = getPathStrategy(context, authority);
- return strategy.getUriForFile(file);
- }
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues values) {
- throw new UnsupportedOperationException("No external inserts");
- }
- @Override
- public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
- @Nullable String[] selectionArgs) {
- throw new UnsupportedOperationException("No external updates");
- }
- @Override
- public int delete(@NonNull Uri uri, @Nullable String selection,
- @Nullable String[] selectionArgs) {
- // ContentProvider has already checked granted permissions
- final File file = mStrategy.getFileForUri(uri);
- return file.delete() ? 1 : 0;
- }
- @Override
- public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
- @Nullable String[] selectionArgs,
- @Nullable String sortOrder) {
- // ContentProvider has already checked granted permissions
- final File file = mStrategy.getFileForUri(uri);
- if (projection == null) {
- projection = COLUMNS;
- }
- String[] cols = new String[projection.length];
- Object[] values = new Object[projection.length];
- int i = 0;
- for (String col : projection) {
- if (OpenableColumns.DISPLAY_NAME.equals(col)) {
- cols[i] = OpenableColumns.DISPLAY_NAME;
- values[i++] = file.getName();
- } else if (OpenableColumns.SIZE.equals(col)) {
- cols[i] = OpenableColumns.SIZE;
- values[i++] = file.length();
- }
- }
- cols = copyOf(cols, i);
- values = copyOf(values, i);
- final MatrixCursor cursor = new MatrixCursor(cols, 1);
- cursor.addRow(values);
- return cursor;
- }
- @Override
- public String getType(@NonNull Uri uri) {
- final File file = mStrategy.getFileForUri(uri);
- final int lastDot = file.getName().lastIndexOf('.');
- if (lastDot >= 0) {
- final String extension = file.getName().substring(lastDot + 1);
- final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
- if (mime != null) {
- return mime;
- }
- }
- return "application/octet-stream";
- }
- }
任何一個ContentProvider都需要繼承ContentProvider類,然后實現(xiàn)這幾個抽象方法:
onCreate,getType,query,insert,delete,update。
(其中每個方法中的Uri參數(shù),就是我們之前通過getUriForFile方法生成的content URI)
我們分三部分說說:
數(shù)據(jù)調(diào)用方面
其中,query,insert,delete,update四個方法就是數(shù)據(jù)的增刪查改,也就是進(jìn)程間通信的相關(guān)方法。
其他應(yīng)用可以通過ContentProvider來調(diào)用這幾個方法,來完成對本地應(yīng)用數(shù)據(jù)的增刪查改,從而完成進(jìn)程間通信的功能。
具體方法就是調(diào)用getContentResolver()的相關(guān)方法,例如:
- Cursor cursor = getContentResolver().query(uri, null, null, null, "userid");
再回去看看FileProvider:
- query,查詢方法。在該方法中,返回了File的name和length。
- insert,插入方法。沒有做任何事。
- delete,刪除方法。刪除Uri對應(yīng)的File。
- update,更新方法。沒有做任何事。
MIME類型
再看getType方法,這個方法主要是返回 Url所代表數(shù)據(jù)的MIME類型。
一般是使用默認(rèn)格式:
- 如果是單條記錄返回以vnd.android.cursor.item/ 為首的字符串
- 如果是多條記錄返回vnd.android.cursor.dir/ 為首的字符串
具體怎么用呢?可以通過Content URI對應(yīng)的ContentProvider配置的getType來匹配Activity。
有點拗口,比如Activity和ContentProvider這么配置的:
- android:name=".SecondActivity">
- @Override
- public String getType(@NonNull Uri uri) {
- return "type_test";
- }
- intent.setData(mContentRUI);
- startActivity(intent)
這樣配置之后,startActivity就會檢查Activity的mineType 和 Content URI 對應(yīng)的ContentProvider的getType是否相同,相同情況下才能正常打開Activity。
初始化
最后再看看onCreate方法。
在APP啟動流程中,自動執(zhí)行所有ContentProvider的attachInfo方法,并最后調(diào)用到onCreate方法。一般在這個方法中就做一些初始化工作,比如初始化ContentProvider所需要的數(shù)據(jù)庫。
而在FileProvider中,調(diào)用了attachInfo方法作為了一個初始化工作的入口,其實和onCreate方法的作用一樣,都是App啟動的時候會調(diào)用的方法。
在這個方法中,也是限制了exported屬性必須為false,grantUriPermissions屬性必須為true。
- if (info.exported) {
- throw new SecurityException("Provider must not be exported");
- }
- if (!info.grantUriPermissions) {
- throw new SecurityException("Provider must grant uri permissions");
- }
這個初始化方法和特性,也是被很多三方庫所利用,可以進(jìn)行靜默無感知的初始化工作,而無需單獨調(diào)用三方庫初始化方法。比如Facebook SDK:
- android:name="com.facebook.internal.FacebookInitProvider"
- android:authorities="${applicationId}.FacebookInitProvider"
- android:exported="false" />
- public final class FacebookInitProvider extends ContentProvider {
- private static final String TAG = FacebookInitProvider.class.getSimpleName();
- @Override
- @SuppressWarnings("deprecation")
- public boolean onCreate() {
- try {
- FacebookSdk.sdkInitialize(getContext());
- } catch (Exception ex) {
- Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex);
- }
- return false;
- }
- //...
- }
這樣一寫,就無需單獨集成FacebookSDK的初始化方法了,實現(xiàn)靜默初始化。
而Jetpack中的App Startup也是考慮到這些三方庫的需求,對三方庫的初始化進(jìn)行了一個合并,從而優(yōu)化了多次創(chuàng)建ContentProvider的耗時。
拿到Content URI 該怎么使用?
很多人都知道該怎么配置FileProvider讓別人(比如照相APP)來獲取我們的Content URI,但是你們知道別人拿到Content URI之后又是怎么獲取具體的File的呢?
其實仔細(xì)找找就能發(fā)現(xiàn),在FileProvider.java中有注釋說明:
- The client app that receives the content URI can open the file and access its contents by calling
- {@link android.content.ContentResolver#openFileDescriptor(Uri, String) ContentResolver.openFileDescriptor}
- to get a {@link ParcelFileDescriptor}
也就是openFileDescriptor方法,拿到ParcelFileDescriptor類型數(shù)據(jù),其實就是一個文件描述符,然后就可以讀取文件流了。
- ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(intent.getData(), "r");
- FileReader reader = new FileReader(parcelFileDescriptor.getFileDescriptor());
- BufferedReader bufferedReader = new BufferedReader(reader);
ContentProvider 實際應(yīng)用
在平時的工作中,主要有以下以下幾種情況和ContentProvider打交道比較多:
- 和系統(tǒng)的一些App通信,比如獲取通訊錄,調(diào)用拍照等。上述的FileProvider也是屬于這種情況。
- 與自己的APP有一些交互。比如自家多應(yīng)用之間,可以通過這個進(jìn)行一些數(shù)據(jù)交互。
- 三方庫的初始化工作。很多三方庫會利用ContentProvider自動初始化這一特性,進(jìn)行一個靜默無感知的初始化工作。
總結(jié)
ContentProvider作為四大組件之一,似乎并沒有其他組件的存在感那么強(qiáng)。
但是他還是有自己的那一份職責(zé),也就是在保證安全的情況下進(jìn)行應(yīng)用間通信,還可以擴(kuò)展作為幫助初始化的組件。所以了解他,掌握它也是很重要的,沒準(zhǔn)以后哪個時候你就需要他了。
不要忽視任何一個知識點。
參考
https://mp.weixin.qq.com/s/kQmH2GnwW8FK-yNmWcheTA
https://segmentfault.com/a/1190000021357383
https://blog.csdn.net/lmj623565791/article/details/72859156
本文轉(zhuǎn)載自微信公眾號「碼上積木」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系碼上積木公眾號。
當(dāng)前題目:透過FileProvider再看ContentProvider
轉(zhuǎn)載源于:http://www.dlmjj.cn/article/cdsscdj.html


咨詢
建站咨詢
