Android 8.0 targetsdkversion升级到26填坑
目錄
前言
1、動(dòng)態(tài)權(quán)限管理
2、ContentResolver
3、FileProvider(File URI)
4、DownloadManager(ContentResolver.openFileDescriptor)
5、后臺(tái)service
6、集合API變更
7、通知Notification
8、隱式廣播
9、懸浮窗
前言
近期因?yàn)閼?yīng)用市場(chǎng)要求,需要將targetsdkversion升級(jí)到26
之前博客中我們了解過targetsdkversion的重要性,當(dāng)時(shí)我們建議輕易不要改動(dòng)這個(gè)參數(shù)。
但是這次因?yàn)閼?yīng)用市場(chǎng)的硬性要求,我們必須做升級(jí),那么就需要面對(duì)升級(jí)后帶來的兼容性問題。
1、動(dòng)態(tài)權(quán)限管理
最明顯的問題就是權(quán)限管理,在6.0加入的動(dòng)態(tài)權(quán)限需要我們手動(dòng)進(jìn)行處理。這個(gè)就老生常談了,這里不展開說了。
2、ContentResolver
處理完權(quán)限我們運(yùn)行程序后,發(fā)現(xiàn)app竟然crash了,報(bào)錯(cuò):
java.lang.SecurityException: Failed to find provider?xxx?for user 0; expected to find a valid ContentProvider for this authority
調(diào)查發(fā)現(xiàn)我們使用了
getContentResolver().registerContentObserver(uri, false, observer)或?
getContentResolver().notifyChange(uri, observer)查詢相關(guān)文檔得知8.0對(duì)ContentResolver加了一層安全機(jī)制,防止外部訪問app內(nèi)部使用的數(shù)據(jù)。
那么怎么解決這個(gè)問題?
首先我們需要自定義一個(gè)ContentProvider,如果僅僅是為了通知,可以不實(shí)現(xiàn)抽象函數(shù)
然后在AndroidManifest中
最后在使用時(shí)uri需要是content://<authorities>/...形式,如
getContentResolver().registerContentObserver(new URI().parse("content://xxxx/tablename"), false, observer) getContentResolver().notifyChange(new URI().parse("content://xxxx/tablename"), null)3、FileProvider(File URI)
在8.0(實(shí)際是7.0)下,當(dāng)app對(duì)外傳遞file URI的時(shí)候會(huì)導(dǎo)致一個(gè)FileUriExposedException。
比如說在app拉起安裝apk,之前代碼是:
Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(Uri.parse(file), "application/vnd.android.package-archive"); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i);當(dāng)tagsdkversion升級(jí)到26就會(huì)出現(xiàn)問題。這是因?yàn)?.0添加了一項(xiàng)安全機(jī)制,app不再允許對(duì)外暴露File URI(file://URI),而用Content URI(content://URI)來代替,Content URI會(huì)授予URI臨時(shí)訪問權(quán)限,提高文件訪問的安全性。
那么如何使用Content URI?
添加FileProvider即可,它是ContentProvider的一個(gè)子類。
首先,在res的xml目錄(沒有則新建)下,新增一個(gè)文件file_provider_paths.xml:
<?xml version="1.0" encoding="utf-8"?> <paths><external-pathname="apkDownload"path="download"/> </paths>這里注意paths一定要小寫,大寫也不會(huì)報(bào)錯(cuò),但是會(huì)造成一些麻煩;再有path不能為空。
這里就要詳細(xì)解釋一下:
- name是Content URI對(duì)外暴露的偽路徑,它對(duì)應(yīng)這path
- path則是真實(shí)路徑
- external-path則表示sd卡,這里幾種選擇,如下:
所以在使用要格外注意,一定要與真實(shí)地址對(duì)應(yīng)上。比如真實(shí)地址為/data/data/<package-name>/cache/apk/1.apk
那么就是:
<cache-pathname="apkDownload"path="apk"/>而最終得到的Content URI則是content://<authorities>/apkDownload/1.apk
可以看到使用authorities(在后面)和name隱藏了真實(shí)路徑,這樣就防止對(duì)外暴露了路徑
其次,在AndroidManifest中添加:
<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="<packageName>.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_provider_paths" /> </provider>exported和grantUriPermissions都不能缺,否則會(huì)引起錯(cuò)誤
這里的authorities就是最后Content URI中的,而且后面還會(huì)使用到
然后,我需要重寫拉起安裝的代碼,如下:
Intent i = new Intent(Intent.ACTION_VIEW); Uri contentUri; if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);contentUri = FileProvider.getUriForFile(context, authorities, file); } else{contentUri = Uri.parse(file); } i.setDataAndType(contentUri, "application/vnd.android.package-archive"); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i);注意這里的authorities一定要與AndroidManifest中的保持一致。
最后再記錄一下開發(fā)過程中遇到的幾個(gè)問題:
(1)InstallStart: Requesting uid 10087 needs to declare permission android.permission.REQUEST_INSTALL_PACKAGES
? ?在8.0以上,需要在AndroidManifest中添加<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>權(quán)限
(2)安裝包解析失敗
???這里有兩種情況,可以根據(jù)日志分析出來:
? ?(1)路徑錯(cuò)誤:日志中有No such file or directory這樣的字眼。檢查文件路徑和上面的配置是否有誤,比如文件在sd卡而xml中是應(yīng)用cache中。
? ?(2)權(quán)限問題,日志如下:
? ? ? ?W/System.err: java.lang.SecurityException: Permission Denial: opening provider ... from ProcessRecord ... that is not exported from .?
? ? ? ?W/PackageInstaller: InstallStaging:Error staging apk from content URIPermission Denial: opening provider?... from ProcessRecord?... that is not exported from?...
? ? ? ? ? ? ?網(wǎng)上對(duì)有不少說法:不能使用sd卡,必須用應(yīng)用空間;還有將android:exported設(shè)為true。事實(shí)證明都不對(duì),尤其android:exported設(shè)為true會(huì)造成java.lang.SecurityException:?Provider?must?not?be?exported錯(cuò)誤。
? ? ? ? ? ? ?這個(gè)問題實(shí)際上是沒有給intent添加Intent.FLAG_GRANT_READ_URI_PERMISSION這個(gè)flag。尤其要注意,因?yàn)檫@里要添加兩個(gè)flag,一定要使用addFlags。如果使用setFlags,由于Intent.FLAG_ACTIVITY_NEW_TASK在后面設(shè)置,會(huì)丟失前面的flag,會(huì)導(dǎo)致上面的問題。
(3)FileProvider沖突
當(dāng)lib或module中的AndroidManifest添加了FileProvider,而主項(xiàng)目也需要添加時(shí),就會(huì)出現(xiàn)沖突,gradle編譯錯(cuò)誤如下:
Error:C:***AndroidManifest.xml:352:13-62 Error:
Attribute provider#android.support.v4.content.FileProvider@authorities value=(***.fileProvider) from AndroidManifest.xml:352:13-62
is also present at [xxxx:xxx] AndroidManifest.xml:19:13-64 value=(***.fileprovider).
Suggestion: add 'tools:replace="android:authorities"' to <provider> element at AndroidManifest.xml:350:9-358:20 to override.
錯(cuò)誤上建議我們添加tools:replace="android:authorities"來解決問題,實(shí)際上我發(fā)現(xiàn)這并不能很好的解決問題。那么怎么辦?
簡(jiǎn)單的方法是我們自定義一個(gè)類,繼承FileProvider,在AndroidManifest中使用這個(gè)自定義類,這樣就可以避免沖突了。如:
<providerandroid:name=".MyFileProvider"android:authorities="<packageName>.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_provider_paths" /> </provider>4、DownloadManager(ContentResolver.openFileDescriptor)
同樣在7.0上,為了安全起見對(duì)DownloadManager也做了修改,拋棄了COLUMN_LOCAL_FILENAME字段。
在之前我們使用DownloadManager,會(huì)使用一個(gè)receiver來接收下載結(jié)束并處理后續(xù),代碼如下:
@Override public void onReceive(Context context, Intent intent) {DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);if (downloadId == mId) {DownloadManager.Query query = new DownloadManager.Query();query.setFilterById(mId);Cursor cursor = downloadManager.query(query);if (cursor.moveToFirst()) {int urlId = cursor.getColumnIndex(DownloadManager.COLUMN_URI);int stateId = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);String url = cursor.getString(urlId);int state = cursor.getInt(stateId);int pathId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);tmp = cursor.getString(pathId);...可以看到從cursor里可以得到下載狀態(tài),url及下載地址等信息。
但是當(dāng)targetsdkversion升級(jí)到7.0或以上后,在7.0及以上機(jī)器上就會(huì)crash,報(bào)錯(cuò)如下:
java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
如上所說,為了安全起見拋棄了COLUMN_LOCAL_FILENAME(還有COLUMN_LOCAL_URI字段),讓我們用ContentResolver.openFileDescriptor()代替。
那么ContentResolver.openFileDescriptor()又是什么?
簡(jiǎn)單來說會(huì)得到一個(gè)ParcelFileDescriptor對(duì)象,并可以進(jìn)一步得到一個(gè)FileDescriptor對(duì)象。
我們可以通過它們打開文件流,比如:
(使用FileDescriptor還有其它方法,都是通過流來處理)
那么我們?nèi)绻恍枰螺d文件的完整路徑即可,這時(shí)就不需要openFileDescriptor,只需要將上面獲取下載文件的兩行代碼替換為:
在7.0之上處理一下就可以了。
5、后臺(tái)service
這里有兩點(diǎn)需要注意:
(1)當(dāng)app在后臺(tái)時(shí),startService啟動(dòng)一個(gè)background service將不再允許,會(huì)導(dǎo)致crash。bindService不受影響。
解決方法是避免app不在前端時(shí)啟動(dòng)服務(wù);或者如果一定需要app在后臺(tái)啟動(dòng)服務(wù),請(qǐng)啟動(dòng)一個(gè)foreground?service,但是需要一個(gè)常駐的notifacation; 或者使用bindService來啟動(dòng)服務(wù)。具體做法有很多文章,這里就不詳細(xì)寫了。
(2)service的存活差異
經(jīng)測(cè)試發(fā)現(xiàn),targetsdkversion的改變對(duì)service(background service)的存活也是有影響的。
這塊我有一篇詳細(xì)的文章來講解,請(qǐng)見《探討8.0版本下后臺(tái)service存活機(jī)制及保活》
對(duì)于targetsdkversion 26的app在8.0及以上版本,想要長(zhǎng)時(shí)間存活,最好的方式就是使用foreground?service;或者將service綁定到application上bindService;另外一個(gè)解決方案就是請(qǐng)求加入耗電白名單,但是這個(gè)對(duì)用戶不友好。
6、集合API變更
在android8.0上AbstractCollection.removeAll(null)和AbstractCollection.retainAll(null)會(huì)引發(fā)NullPointerException;之前版本則不會(huì)。所以我們?cè)谑褂眠@兩個(gè)函數(shù)前要確保參數(shù)不是null,必要是需要判空。
可以看看這兩個(gè)函數(shù)的代碼:
public boolean removeAll(Collection<?> c) {Objects.requireNonNull(c);boolean modified = false;Iterator<?> it = iterator();while (it.hasNext()) {if (c.contains(it.next())) {it.remove();modified = true;}}return modified; } public boolean retainAll(Collection<?> c) {Objects.requireNonNull(c);boolean modified = false;Iterator<E> it = iterator();while (it.hasNext()) {if (!c.contains(it.next())) {it.remove();modified = true;}}return modified; }可以看到都在首行調(diào)用了Objects.requireNonNull(c),這個(gè)代碼是:
public static <T> T requireNonNull(T obj) {if (obj == null)throw new NullPointerException();return obj; }可以看到如果是null就會(huì)拋出一個(gè)空指針錯(cuò)誤。
7、通知Notification
在Android8.0上,通知做了較大的改動(dòng),增加了分組和渠道機(jī)制,這樣更加方便了用戶對(duì)通知的管理。
那么我們app中的notification相關(guān)代碼就需要變動(dòng),否則可以無法發(fā)出通知。
我們需要為notification增加分組和渠道,如下:
String groupId =?"group1";NotificationChannelGroup group =?new?NotificationChannelGroup(groupId,?"");notificationManager.createNotificationChannelGroup(group);String channelId =?"channel1";NotificationChannel channel =?new?NotificationChannel(channelId,"推廣信息", NotificationManager.IMPORTANCE_DEFAULT);channel.setDescription("推廣信息");channel.setGroup(groupId);notificationManager.createNotificationChannel(channel);NotificationCompat.Builder builder =new NotificationCompat.Builder(context, channelId); //上面使用support包中的NotificationCompat,但是版本需要是26及以上 //或者不使用support包中的類,直接使用Notification.Builder,但是要進(jìn)行版本判斷//Notification.Builder builder; //if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // ? ?builder = new Notification.Builder(BaseApp.getAppContext(), App.CHANNEL_ID); //} //else{ // ? ?builder = new Notification.Builder(BaseApp.getAppContext()); //}... notificationManager(notificationId, builder.build())這樣通知就能正常發(fā)出并展示了,但是其實(shí)group并不是必須的,所以可以只設(shè)置channel,如:
String channelId =?"channel1";NotificationChannel adChannel =?new?NotificationChannel(channelId,"推廣信息", NotificationManager.IMPORTANCE_DEFAULT);adChannel.setDescription("推廣信息");notificationManager.createNotificationChannel(adChannel);NotificationCompat.Builder builder =new NotificationCompat.Builder(context, channelId); ... notificationManager(notificationId, builder.build())
8、隱式廣播
android8.0之后靜態(tài)注冊(cè)(manifest中)的隱式廣播將不再起作用,但是有一些隱式廣播除外,靜態(tài)注冊(cè)它們?nèi)匀豢梢越邮盏綇V播。
解決方法是將隱式廣播改成動(dòng)態(tài)注冊(cè)。
靜態(tài)注冊(cè)的顯式廣播不受影響
9、懸浮窗
使用SYSTEM_ALERT_WINDOW 權(quán)限的應(yīng)用無法再使用以下窗口類型來在其他應(yīng)用和系統(tǒng)窗口上方顯示提醒窗口:
??? ?TYPE_PHONE
??? ?TYPE_PRIORITY_PHONE
??? ?TYPE_SYSTEM_ALERT
??? ?TYPE_SYSTEM_OVERLAY
??? ?TYPE_SYSTEM_ERROR
??? 相反,應(yīng)用必須使用名為 TYPE_APPLICATION_OVERLAY 的新窗口類型。
所以我們要在代碼中判斷版本:
if (Build.VERSION.SDK_INT>=26) {windowParams.type= WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; }else{windowParams.type= WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; }
而且在manifest添加權(quán)限后,如果在6.0及以上系統(tǒng)中還需要?jiǎng)討B(tài)請(qǐng)求相關(guān)權(quán)限:
總結(jié)
以上是生活随笔為你收集整理的Android 8.0 targetsdkversion升级到26填坑的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android中几种定位方式详解
- 下一篇: Unity的Flutter——UIWid