前言
HI萍悴,歡迎來到《每周一博》。今天是十月第三周寓免,我給大家分享一下安卓各系統(tǒng)版本的新特性和開發(fā)過程中的適配.
安卓每次升級(jí)都會(huì)帶來一些重大的改變癣诱,在性能,安全上都會(huì)有所提升袜香,每個(gè)版本也都有其各自的特點(diǎn)撕予,比如L引入的MaterialDesign設(shè)計(jì),M增加對(duì)權(quán)限和耗電的控制蜈首,N支持的多窗口和VR实抡,O對(duì)后臺(tái)進(jìn)程的限制欠母,P對(duì)流海屏的支持等。
當(dāng)然吆寨,每個(gè)版本都需要開發(fā)者去適配赏淌,開發(fā)者明顯可以感到系統(tǒng)對(duì)權(quán)限的收緊和安全上的重視,過去很多粗放的代碼都要變更啄清,典型的比如M的權(quán)限適配六水,N的FileProvider,O的后臺(tái)限制盒延,P的安全上的配配等缩擂,這里我將針對(duì)LMNOP這5個(gè)版本中重大的適配做一個(gè)簡(jiǎn)單總結(jié)鼠冕。
安卓各版本新特性
安卓L
- 引入Material Design設(shè)計(jì)添寺,增加UI控件和動(dòng)畫效果, V7支持庫(kù)中增加新控件以便向下兼容懈费;
- 系統(tǒng)由以往的Dalvik模式改為采用ART(Android Runtime)模式计露,實(shí)現(xiàn)ahead-of-time (AOT)靜態(tài)編譯與just-in-time (JIT)動(dòng)態(tài)編譯交互進(jìn)行;
- 支持64位系統(tǒng)憎乙;
安卓M
- 新增運(yùn)行時(shí)權(quán)限概念:系統(tǒng)默認(rèn)給app授權(quán)部分基礎(chǔ)權(quán)限票罐,其他敏感權(quán)限,需要運(yùn)行時(shí)手動(dòng)請(qǐng)求系統(tǒng)授予權(quán)限泞边;
- 新增Doze模式:可以自動(dòng)識(shí)別手機(jī)使用狀態(tài)该押,并在閑時(shí)主動(dòng)關(guān)閉部分后臺(tái)進(jìn)程以節(jié)省能耗;
- 新增待機(jī)模式: 針對(duì)很少使用的應(yīng)用阵谚,將不再消耗電量
- 新增指紋解鎖API蚕礼;
安卓N:
- 通知中心變的便捷且更強(qiáng)大,下拉通知欄中最上方加入了快捷按鍵控制開關(guān)梢什,同時(shí)通知中心能顯示更多的信息奠蹬,用戶可以在通知中心內(nèi)快速回復(fù);
- 支持多窗口功能嗡午;
- 支持VR囤躁,以使開發(fā)者能為用戶打造高質(zhì)量移動(dòng) VR 體驗(yàn);
- 引入全新的JIT編譯器荔睹,使得App安裝速度快了75%狸演,編譯代碼的規(guī)模減少了50%;
- App快捷菜單僻他,長(zhǎng)按APP圖標(biāo)可以快速進(jìn)入某些功能界面宵距;
安卓O:
- 支持畫中畫模式;
- 自動(dòng)填充中姜,對(duì)于用戶設(shè)備上最常用的應(yīng)用消玄,Android O將會(huì)幫助用戶進(jìn)行快速登錄跟伏,而不用每次都填寫賬戶名和密碼;
- 自適應(yīng)圖標(biāo)翩瓜,為開發(fā)者提供了適應(yīng)其顯示設(shè)備的每個(gè)圖標(biāo)的多個(gè)形狀模板受扳,來解決Android中APP圖標(biāo)形狀不一致的問題;
- 通知提醒兔跌,當(dāng)通知欄有未讀信息時(shí)勘高,會(huì)出現(xiàn)一個(gè)小點(diǎn),這時(shí)候長(zhǎng)按應(yīng)用程序圖標(biāo)坟桅,就會(huì)以類似氣泡的形式快速預(yù)覽华望。
- 后臺(tái)進(jìn)程限制,當(dāng)應(yīng)用被置入后臺(tái)后仅乓,將自動(dòng)智能限制后臺(tái)應(yīng)用活動(dòng)赖舟,主要會(huì)限制應(yīng)用的廣播,后臺(tái)運(yùn)行和位置夸楣,但應(yīng)用的整體進(jìn)程并沒有被殺掉宾抓。
- 運(yùn)行時(shí)權(quán)限策略變化,系統(tǒng)只會(huì)授予應(yīng)用明確請(qǐng)求的權(quán)限豫喧,而一旦用戶為應(yīng)用授予某個(gè)權(quán)限石洗,則所有后續(xù)對(duì)該權(quán)限組中權(quán)限的請(qǐng)求都將被自動(dòng)批準(zhǔn)。
安卓P:
- 支持流海屏紧显,系統(tǒng)會(huì)管理狀態(tài)欄的高度從而將內(nèi)容與裁切區(qū)域分開讲衫,如果擁有重要的沉浸式內(nèi)容,則還可以使用新的API查看裁切形狀并創(chuàng)建全屏布局孵班;
- 多攝像頭API涉兽,可以通過兩個(gè)或更多實(shí)體攝像頭同時(shí)訪問視頻流;
- 新增了ImageDecoder類重父,為解碼圖像提供了一種更優(yōu)的方法花椭;
- 增加神經(jīng)網(wǎng)絡(luò)API-1.1版本;
安卓各版本開發(fā)適配
安卓L-5.0
- Service服務(wù)必須采用顯示方式啟動(dòng)
解決辦法有兩種房午,一種是設(shè)置Action和packageName矿辽,這也是推薦的方案,另一種是通過PackageManager遍歷所有符合的ComponentName郭厌;
Intent intent = new Intent();
// service定義的action
intent.setAction("XXX.XXX.XXX");
// service所在的包名
intent.setPackage(getPackageName());
context.startService(mIntent);
- 通知欄適配
5.0以上需要通過NotificationCompat.Builder來創(chuàng)建通知袋倔,而不是直接new Notification或new Notification.Builder,另外在8.0之上還需要適配折柠,后面再說宾娜;
Notification notification = new NotificationCompat.Builder(mContext)
.setContentTitle(name)
.setContentText(contentText)
.setSmallIcon(R.drawable.stat)
.setContentIntent(pi)
.build();
- 關(guān)閉通知權(quán)限不彈Toast
其實(shí)看過Toast源碼就知道它是通知欄服務(wù)NotificationManagerService維護(hù)的一個(gè)隊(duì)列,通過調(diào)用 WindowManager來添加view扇售,所以關(guān)閉通知權(quán)限后無法顯示Toast前塔。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
}
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
解決辦法可以自己去實(shí)現(xiàn)一個(gè)Toast嚣艇,使用隊(duì)列來維護(hù),用WindowManager來addView华弓,或者直接使用第三方庫(kù)食零。
- View的高度與陰影
View新增加了z軸屬性,來體現(xiàn)MaterialDesign中的層次寂屏,影響的因素有translationZ和elevation贰谣。
計(jì)算View高度= elevation + translationZ
(1) transtionZ屬性表示view在Z方向移動(dòng)的距離,一般用于屬性動(dòng)畫中迁霎;
(2) elevation表示view的陰影吱抚,可以在xml中直接使用屬性, 也可以在代碼中使用view.setEvelvation()考廉;elevation也能起到權(quán)重的作用秘豹,因?yàn)橛嘘幱暗谋厝皇窃谄胀▽蛹?jí)的上面,具體的關(guān)于elevation和transtionZ的說明可以參考這篇文章芝此。
高度會(huì)影響View的繪制順序憋肖,以前是按View添加順序繪制因痛,現(xiàn)在高度小的先繪制婚苹,因?yàn)閷蛹?jí)低,在下面鸵膏, 高度相同的膊升,按添加順序繪制,需要注意的是如果View的背景色為透明谭企,則不會(huì)顯示出陰影效果廓译,另外只有子View的大小比父View小時(shí),陰影才能顯示出來债查;
安卓M-6.0
動(dòng)態(tài)權(quán)限申請(qǐng)
當(dāng)targetSdkVersion>=23的時(shí)候, 需要用checkSelfPermission()用來檢測(cè)App是否被授予了權(quán)限非区,比如定位的時(shí)候需要先去檢查是否擁有相關(guān)權(quán)限,如果沒有盹廷,可以使用requestPermissions()用來請(qǐng)求權(quán)限 征绸,這是Activity的方法,同時(shí)在回調(diào)方法onRequestPermissionsResult里面判斷結(jié)果俄占。棄用HttpClient
Android6.0版本移除了對(duì)Httpclient的支持管怠,建議我們使用HttpUrlConnection,如果仍要堅(jiān)持使用缸榄,需要在build.gradle中添加如下代碼:
android {
useLibrary 'org.apache.http.legacy'
}
Doze模式對(duì)定時(shí)器的影響
在Doze模式下渤弛,標(biāo)準(zhǔn) AlarmManager鬧鈴,包括 setExact() 和 setWindow()將推遲到下一維護(hù)時(shí)段甚带,如果需要設(shè)置在低電耗模式下觸發(fā)的鬧鈴她肯,可以使用setAndAllowWhileIdle() 或setExactAndAllowWhileIdle()方法佳头,這2個(gè)方法只能在15分鐘喚醒一次,如果你的廣播需要1分鐘廣播很多次晴氨,也只能15分鐘一次畜晰。setAlarmClock()將不會(huì)受Doze模式影響,但是它會(huì)耗電瑞筐, 所以必須在節(jié)約電量和業(yè)務(wù)之間做個(gè)取舍凄鼻。獲取硬件標(biāo)識(shí)符需要權(quán)限
WifiInfo.getMacAddress()和BluetoothAdapter.getAddress()將始終返回02:00:00:00:00:00,為了在M上可以掃描WiFi或藍(lán)牙聚假,需具有ACCESS_FINE_LOCATION和 ACCESS_COARSE_LOCATION權(quán)限
安卓N-7.0
私有文件權(quán)限收緊
私有文件的文件權(quán)限不再放權(quán)給全部的應(yīng)用块蚌,使用 MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 進(jìn)行的操作將觸發(fā) SecurityException。文件共享使用FileProvider
在N上Android 框架強(qiáng)制運(yùn)行了StrictMode膘格,禁止對(duì)外公開file:// URI峭范,也就是說不能發(fā)起包含文件file:// URI類型的Intent ,這樣會(huì)出現(xiàn)FileUriExposedException異常瘪贱,比如調(diào)用系統(tǒng)相機(jī)拍照或相冊(cè)纱控。
(1) 先在清單文件里面配置FileProvider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
其中exported必須為false,true會(huì)報(bào)安全異常菜秦,grantUriPermissions為true表示授予URI訪問權(quán)限甜害。
(2) 指定共享的文件夾
在res文件夾下創(chuàng)建一個(gè)xml文件夾,然后創(chuàng)建一個(gè)名為“file_paths”(和注冊(cè)的Provider所引用的resource保持一致就可以)的資源文件球昨,內(nèi)容如下;
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="." name="files_root"/>
</paths>
</resources>
代碼中path="."尔店,表示根文件夾,也就是說你能夠向其它的應(yīng)用共享根文件夾及其子文件夾下的任何文件主慰,假設(shè)你把path設(shè)為path="pictures"嚣州, 那么它表示根文件夾下的pictures文件夾,如/storage/emulated/0/pictures共螺,那么向其它應(yīng)用分享pictures文件夾之外的文件是不行的该肴。
path下必須包含一到多個(gè)子元素,這些子元素用于指定共享文件的目錄路徑藐不,必須是這些元素之一:
- <files-path>:內(nèi)部存儲(chǔ)空間應(yīng)用私有目錄下的 files/ 目錄匀哄,等同于 Context.getFilesDir() 所獲取的目錄路徑;
- <cache-path>:內(nèi)部存儲(chǔ)空間應(yīng)用私有目錄下的 cache/ 目錄佳吞,等同于 Context.getCacheDir() 所獲取的目錄路徑拱雏;
- <external-path>:外部存儲(chǔ)空間根目錄,等同于 Environment.getExternalStorageDirectory() 所獲取的目錄路徑底扳;
- <external-files-path>:外部存儲(chǔ)空間應(yīng)用私有目錄下的 files/ 目錄铸抑,等同于 Context.getExternalFilesDir(null) 所獲取的目錄路徑;
- <external-cache-path>:外部存儲(chǔ)空間應(yīng)用私有目錄下的 cache/ 目錄衷模,等同于 Context.getExternalCacheDir()鹊汛;
可以看出蒲赂,這五種子元素基本涵蓋內(nèi)外存儲(chǔ)空間所有目錄路徑,包含應(yīng)用私有目錄刁憋。每個(gè)子元素都擁有 name 和 path 兩個(gè)屬性滥嘴,path 屬性用于指定當(dāng)前子元素所代表目錄下需要共享的子目錄名稱。注意path屬性值不能使用具體的獨(dú)立文件名至耻,只能是目錄名若皱,而name屬性用于給 path 屬性所指定的子目錄名稱取一個(gè)別名。后續(xù)生成content:// URI 時(shí)尘颓,會(huì)使用這個(gè)別名代替真實(shí)目錄名走触。這樣做的目的,很顯然是為了提高安全性疤苹。
如果我們需要分享的文件位于同級(jí)別目錄下不同的子目錄中互广,就需要添加多個(gè)子元素逐一指定要分享的文件目錄,或者共享他們通用的父目錄也行卧土。
(3) 使用FileProvider惫皱,比如調(diào)用相機(jī)
String filePath = Environment.getExternalStorageDirectory() +
"/images/"+System.currentTimeMillis()+".jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdir();
}
// 這里的uri要和manifest里面的對(duì)應(yīng)上
Uri contentUri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID + ".myprovider", outputFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_TAKE_PICTURE);
安卓O-8.0
-
通知欄增加渠道
安卓O增加了渠道,使得用戶可以選擇接受哪些渠道的通知尤莺,上一張圖比較直觀旅敷。
這里我上一份最終的通知欄適配代碼,經(jīng)歷各版本的缝裁;
public void showNotification(){
final int NOTIFICATION_ID = 12234;
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
Intent intent = new Intent();
String action = "com.tamic.myapp.action";
intent.setAction(action);
Notification notification = null;
String contentText;
PendingIntent pi = PendingIntent.getActivity(mContext, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
notification = new Notification();
notification.icon = android.R.drawable.stat_sys_download_done;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notification.setLatestEventInfo(mContext, aInfo.mFilename, contentText, pi);
// 5.0適配
} else if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= LOLLIPOP_MR1) {
notification = new NotificationCompat.Builder(mContext)
.setContentTitle("Title")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(pi).build();
// 4.4適配
} else if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT <= LOLLIPOP_MR1) {
notification = new Notification.Builder(mContext)
.setAutoCancel(false)
.setContentIntent(pi)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setWhen(System.currentTimeMillis())
.build();
// 8.0適配
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
String CHANNEL_ID = "my_channel_01";
CharSequence name = "my_channel";
String Description = "This is my channel";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
mChannel.setDescription(Description);
mChannel.enableLights(true);
mChannel.setLightColor(Color.RED);
mChannel.enableVibration(true);
mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
mChannel.setShowBadge(false);
// 這里配置了渠道
notificationManager.createNotificationChannel(mChannel);
notification = new NotificationCompat.Builder(ctx, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle("Title").build();
}
notificationManager.notify(NOTIFICATION_ID, notification);
}
- 安裝APK
Android)去除了“允許未知來源”選項(xiàng)扫皱,所以如果我們的App有安裝App的功能,比如檢查更新之類的捷绑,那么會(huì)無法正常安裝。
首先在AndroidManifest文件中添加安裝未知來源應(yīng)用的權(quán)限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
這樣系統(tǒng)會(huì)自動(dòng)詢問用戶完成授權(quán)氢妈,當(dāng)然也可以先使用 canRequestPackageInstalls()查詢是否有此權(quán)限粹污,如果沒有的話使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES這個(gè)action將用戶引導(dǎo)至安裝未知應(yīng)用權(quán)限界面去授權(quán)。
private void installAPK(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安裝應(yīng)用
} else {
//跳轉(zhuǎn)至“安裝未知應(yīng)用”權(quán)限界面首量,引導(dǎo)用戶開啟權(quán)限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}else {
//安裝應(yīng)用
}
}
//接收“安裝未知應(yīng)用”權(quán)限的開啟結(jié)果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
installAPK();
}
}
- 懸浮窗適配
使用 SYSTEM_ALERT_WINDOW 權(quán)限的應(yīng)用必須使用名為TYPE_APPLICATION_OVERLAY 的新窗口類型壮吩,以下窗口類型將無法顯示:
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
需要在之前的基礎(chǔ)上判斷一下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
另外需要有權(quán)限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
安卓P-9.0
前臺(tái)Service權(quán)限
在 Android P中,如果 targeSdkVersion 升級(jí)到 28加缘,使用前臺(tái) Service 必須要申請(qǐng)F(tuán)OREGROUND_SERVICE權(quán)限鸭叙,如果沒有申請(qǐng)?jiān)摍?quán)限,系統(tǒng)會(huì)拋出SecurityException拣宏,該權(quán)限為普通權(quán)限沈贝,申請(qǐng)自動(dòng)授予應(yīng)用。序列號(hào)棄用
在 Android P中勋乾,Build.SERIAL 始終設(shè)置為 "UNKNOWN" 宋下,來保護(hù)用戶的隱私嗡善。如果需要訪問設(shè)備的硬件序列號(hào),需要請(qǐng)求 READ_PHONE_STATE 權(quán)限学歧,然后調(diào)用 getSerial()罩引;默認(rèn)啟用網(wǎng)絡(luò)傳輸層安全協(xié)議 (TLS)
在 Android P 中,默認(rèn)情況下 isCleartextTrafficPermitted() 函數(shù)返回 false枝笨。 如果應(yīng)用需要為特定域名啟用明文袁铐,必須在應(yīng)用的網(wǎng)絡(luò)安全性配置中針對(duì)這些域名將 cleartextTrafficPermitted 顯式設(shè)置為 true。所以無法及時(shí)把服務(wù)器變更為 https 的應(yīng)用横浑,應(yīng)該通過配置文件針對(duì)特定域名允許使用明文傳輸昭躺,也就是http服務(wù)。
先定義配置文件 res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">secure.example.com</domain>
</domain-config>
</network-security-config>
然后在manifest.xml中使用
<application android:networkSecurityConfig="@xml/network_security_config">
- WiFi相關(guān)權(quán)限變更
(1)在Android 8.0和Android 8.1上:三選一
調(diào)用 WifiManager.getScanResults() 需要以下任何一項(xiàng)權(quán)限伪嫁,如果調(diào)用應(yīng)用程序沒有任何這些權(quán)限领炫,則調(diào)用將失敗并顯示 SecurityException。
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
CHANGE_WIFI_STATE
(2)Android 9及更高版本:全部滿足
調(diào)用 WifiManager.startScan() 需要滿足以下所有條件:
具有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 權(quán)限张咳;
具有 CHANGE_WIFI_STATE 權(quán)限帝洪;
調(diào)用 WifiManager.getScanResults() 需要滿足以下所有條件:
具有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 權(quán)限;
具有 ACCESS_WIFI_STATE 權(quán)限脚猾;
設(shè)備上啟用了位置服務(wù)葱峡;
這個(gè)限制條件也適用于getConnectionInfo() 函數(shù),該函數(shù)返回描述當(dāng)前WiFi連接的WifiInfo 對(duì)象龙助。 如果調(diào)用應(yīng)用具有以下權(quán)限砰奕,則只能使用該對(duì)象的函數(shù)來檢索 SSID 和 BSSID 值:
具有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 權(quán)限;
具有 ACCESS_WIFI_STATE 權(quán)限提鸟;
檢索 SSID 或 BSSID 還需要在設(shè)備上啟用位置服務(wù)军援;
結(jié)尾:
本周給大家簡(jiǎn)單介紹了各系統(tǒng)版本的新特性和開發(fā)適配,具體更多的內(nèi)容可以參考其他文章称勋。感謝大家的閱讀胸哥,我們下周再見。