Android 一路走来十来年啦,从青涩懵懂的小女孩变成如今的大姑娘了,甚至很快就会做个大整容,虽然越变越好,但是带来的历史问题也是相当的棘手,屏幕需要适配说实话可以忍忍,但是历史版本的巨大差异我是真的忍不了,太不爽了...
5.0 之后版本的一些特性改变非常大,很多人都不是非常清楚,有必要门清
广播
从 7.0 API 24 开始,Google 为了优化系统环境开始逐步收紧广播的运行
- Android 8.0 引入了新的广播接收器限制,因此您应该移除所有为隐式广播 Intent 注册的广播接收器
- 8.0 开始,禁止静态注册的广播在 onReceive 方法中启动 Service
- 7.0 API 24 开始,静态注册的广播接收器无法监听网络变化:android.net.conn.CONNECTIVITY_CHANGE,以及 ACTION_NEW_PICTURE,ACTION_NEW_VIDEO 的广播
- 8.0 进一步做了限制,除了下面的,其他的广播都不能用静态注册监听了
/ Android 8.0 上不限制的隐式广播
/**
开机广播
Intent.ACTION_LOCKED_BOOT_COMPLETED
Intent.ACTION_BOOT_COMPLETED
*/
"保留原因:这些广播只在首次启动时发送一次,并且许多应用都需要接收此广播以便进行作业、闹铃等事项的安排。"
/**
增删用户
Intent.ACTION_USER_INITIALIZE
"android.intent.action.USER_ADDED"
"android.intent.action.USER_REMOVED"
*/
"保留原因:这些广播只有拥有特定系统权限的app才能监听,因此大多数正常应用都无法接收它们。"
/**
时区、ALARM变化
"android.intent.action.TIME_SET"
Intent.ACTION_TIMEZONE_CHANGED
AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED
*/
"保留原因:时钟应用可能需要接收这些广播,以便在时间或时区变化时更新闹铃"
/**
语言区域变化
Intent.ACTION_LOCALE_CHANGED
*/
"保留原因:只在语言区域发生变化时发送,并不频繁。 应用可能需要在语言区域发生变化时更新其数据。"
/**
Usb相关
UsbManager.ACTION_USB_ACCESSORY_ATTACHED
UsbManager.ACTION_USB_ACCESSORY_DETACHED
UsbManager.ACTION_USB_DEVICE_ATTACHED
UsbManager.ACTION_USB_DEVICE_DETACHED
*/
"保留原因:如果应用需要了解这些 USB 相关事件的信息,目前尚未找到能够替代注册广播的可行方案"
/**
蓝牙状态相关
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED
BluetoothDevice.ACTION_ACL_CONNECTED
BluetoothDevice.ACTION_ACL_DISCONNECTED
*/
"保留原因:应用接收这些蓝牙事件的广播时不太可能会影响用户体验"
/**
Telephony相关
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED
TelephonyIntents.SECRET_CODE_ACTION
TelephonyManager.ACTION_PHONE_STATE_CHANGED
TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED
TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED
*/
"保留原因:设备制造商 (OEM) 电话应用可能需要接收这些广播"
/**
账号相关
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION
*/
"保留原因:一些应用需要了解登录帐号的变化,以便为新帐号和变化的帐号设置计划操作"
/**
应用数据清除
Intent.ACTION_PACKAGE_DATA_CLEARED
*/
"保留原因:只在用户显式地从 Settings 清除其数据时发送,因此广播接收器不太可能严重影响用户体验"
/**
软件包被移除
Intent.ACTION_PACKAGE_FULLY_REMOVED
*/
"保留原因:一些应用可能需要在另一软件包被移除时更新其存储的数据;对于这些应用,尚未找到能够替代注册此广播的可行方案"
/**
外拨电话
Intent.ACTION_NEW_OUTGOING_CALL
*/
"保留原因:执行操作来响应用户打电话行为的应用需要接收此广播"
/**
当设备所有者被设置、改变或清除时发出
DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED
*/
"保留原因:此广播发送得不是很频繁;一些应用需要接收它,以便知晓设备的安全状态发生了变化"
/**
日历相关
CalendarContract.ACTION_EVENT_REMINDER
*/
"保留原因:由日历provider发送,用于向日历应用发布事件提醒。因为日历provider不清楚日历应用是什么,所以此广播必须是隐式广播。"
/**
安装或移除存储相关广播
Intent.ACTION_MEDIA_MOUNTED
Intent.ACTION_MEDIA_CHECKING
Intent.ACTION_MEDIA_EJECT
Intent.ACTION_MEDIA_UNMOUNTED
Intent.ACTION_MEDIA_UNMOUNTABLE
Intent.ACTION_MEDIA_REMOVED
Intent.ACTION_MEDIA_BAD_REMOVAL
*/
"保留原因:这些广播是作为用户与设备进行物理交互的结果:安装或移除存储卷或当启动初始化时(当可用卷被装载)的一部分发送的,因此它们不是很常见,并且通常是在用户的掌控下"
/**
短信、WAP PUSH相关
Telephony.Sms.Intents.SMS_RECEIVED_ACTION
Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION
注意:需要申请以下权限才可以接收
"android.permission.RECEIVE_SMS"
"android.permission.RECEIVE_WAP_PUSH"
*/
"保留原因:SMS短信应用需要接收这些广播"
Google 不可能彻底废掉广播的,Google 推荐我们使用动态注册广播的方式代替静态注册
7.0 改变的进程间共享文件
进程间共享文件就是在 intent 中传一个 file 地址进去,把文件地址提供给别的进程,7.0 之前使用 Uri.fromFile(file) 来生成文件 uri,地址是 file://xxx 路径,但是从 7.0 开始 file://xxx 不能使了,改成 content://xxx,并且不能用使用 Uri.fromFile(file) 生成 uri 路径了,而且要求隐藏路径,用 tag 指代其中的部分路径,所以 google 提供了专用的 API - FileProvider
这个改变涉及到了我们以下几个操作场景:
所有涉及不是在同一个进程内的文件操作都收到影响,intent 里面传 file 路径的妥妥的都跑不了,新的 Uri 路径张这个样子
content://com.zhy.android7.fileprovider/external/20170601-041411.png
我们拿系统相机举例
// 组织路径,生成文件
var path = "${Environment.getExternalStorageDirectory()}/bwlib/pics"
var filePath = File(path)
filePath.mkdirs()
var fileImage = File(path, "G550.png")
val uri = Uri.fromFile(fileImage)
// 组织 intent
var picIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
picIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri)
startActivityForResult(picIntent, 10)
自 7.0 之后不好使用了,除了需要相机,SDK 卡权限之外,另有几处改变:
1. FileProvider
FileProvider 是 ContentProvider 的子类,需要我们在 AndroidManifest 中声明
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.bloodcrown.bw.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
- authorities 的组成是 - 跟包名+fileProvider,这里的包名必须使用 app module 的包名,你的这段 xml 不管写在哪个 module 里面的都必须使用 app module 的包名
- resource 里面的这个 path 声明的是 FileProvider 可以使用的文件路径范围
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path name="root" path="" />
<files-path name="files" path="path" />
<cache-path name="cache" path="path" />
<external-path name="external" path="path" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
</paths>
里面每一个 path 节点都代表了一个文件路径范围,对应一个 API,但是注意上面 path 不为null 的都是必须写的,不能空
一般不用写这么,我就写 external-path 代替 SD 卡就行了
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="bwlib"/>
</paths>
2. 具体代码
上面的代码我们改下就是下面这个样子了
fun startCarme() {
var path = "${Environment.getExternalStorageDirectory()}/bwlib/pics"
var filePath = File(path)
filePath.mkdirs()
var fileImage = File(path, "G550.png")
var fileUri = getFileUri(this, fileImage)
var picIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
picIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri)
picIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
startActivityForResult(picIntent, 10)
}
fun getFileUri(context: Context, file: File): Uri {
if (Build.VERSION.SDK_INT >= 24) {
return FileProvider.getUriForFile(context, "com.bloodcrown.bw.fileProvider", file)
}
return Uri.fromFile(file)
}
- 用 FileProvider.getUriForFile 代替 Uri.fromFile,FileProvider.getUriForFile 中的第二个参数对应 FileProvider 在 xml 中的 authorities 属性,必须一样
- picIntent.addFlags 中添加临时读写权限,只要是有涉及 intent 的都可以直接用 intent .addFlags 的写法,另一种写法如下:
// 2个申请临时权限的 API
grantUriPermission(String toPackage, Uri uri, int modeFlags)
revokeUriPermission(Uri uri, int modeFlags)
// grantUriPermission 需要传递一个包名,就是你要给哪个应用授权
// 但是很多时候,比如分享,我们并不知道最终用户会选择哪个 app,所以我们可以这样
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities
(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, flag);
}
3. 注意点
Uri 路径是非常重要的,路径不对虽然不会 carsh,但是会让我们操作没有反应,比如系统相机吊不起来,拍完照点完成没反应,都是 Uri 路径出错引起的:
- 首先 File 必须书写正确,public File(String parent, String child) 这个构造方法中,parent 是文件夹路径,child 是该文件文件名
- 其次 File 的路径不能超出 FileProvider xml 中的路径范围,比如下面这个:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="bwlib"/>
</paths>
file 必须在这个文件里面 sd卡/bwlib,你的 File 可以是这样的:sd卡/bwlib/111.png;sd卡/bwlib/bbb/111.png,但是这样就不行了 sd卡/111.png
4. 封装
FileProvider 的代码基本都是死的,就是用 File 换一个 Uri 出来,这一看就得搞个工具类出来是不是,具体写多少看大家需求了
object FileUtils {
/**
* 申城 Uri 路径,针对 7.0 之后 File 的适配
*/
@JvmStatic
fun getUriByFile(context: Context, file: File, authority: String): Uri {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return FileProvider.getUriForFile(context, authority, file)
}
return Uri.fromFile(file)
}
}
另外关于 FileProvider 在 AndroidManifest 中的声明,我认为每个 app 都有自己特定的文件夹配置需求,应该交给每个 app 来自己搞定,放到 baselib module 中不是个好选择
8.0 开始安装 APK 需要权限判断了
8.0 开始未知应用安装需要权限了,需要添加下面这个权限,这样系统会自动询问用户完成授权
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
或者用代码做
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
} else {
//跳转至“安装未知应用”权限界面,引导用户开启权限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,selfPackageUri);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}
intent 声明也有一点变化
// 以前使用的 Intent.ACTION_VIEW 不好使了,用下面的
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
// 预防解析包安装失败,下面 Flag 顺序不呢个错,先写 NEW_TASK
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
------------------------------------------------------------------------------------------
File file = new File(Environment.getExternalStorageDirectory(), "testandroid7-debug.apk");
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
startActivity(intent);
file Uri 的适配看上面内容
新版 apk 签名
7.0 开始提供了 APK signature scheme v2
版签名,见下图:
其他不变,签名的时候选择有变化,V1 是旧版,V2是新版
- 只勾选v1签名就是传统方案签署,但是在7.0上不会使用V2安全的验证方式。
- 只勾选V2签名7.0以下会显示未安装,7.0上则会使用了V2安全的验证方式。
- 同时勾选V1和V2则所有版本都没问题
org.apache 不支持问题
// build.gradle里面加上这句话
defaultConfig {
useLibrary 'org.apache.http.legacy'
}
https
9.0 开始明文网络请求不行了,网络请求必须使用 https ,要是不使用 https 可以这样做
<application
android:networkSecurityConfig="@xml/network_security_config">
<!--9.0加的,哦哦-->
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
</application>
// 在 values 字原文件中添加 xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
8.0 notification 添加通知渠道才能显示
8.0 开始开始对 app 内的通知分类,这个类别就是走的 noptificationChannel,用户可以对某一类渠道的通知做处理,是不看也好是不接受也罢,不会对 app 所有通知一刀切了,这算是今后系统发展的必然吧,越来越精细了,适应这种思路很有必要
// 1. build 构造函数都要传渠道号
var build = NotificationCompat.Builder(this, "7602")
// 2. 在 application 中创建渠道,这个渠道是加入到系统设置中去的
// 所以无法修改,只有 add 不能 updata,只有在 app 删除时系统才会删除通知渠道
var notificationManager : NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("7602", "test 渠道",NotificationManager.IMPORTANCE_HIGH)
// 可以添加多个渠道进去
notificationManager.createNotificationChannel(channel)
}
// 3. build 构建参数
var build = NotificationCompat.Builder(this, "7602")
.setContentTitle("Title...")
.setContentText("text...")
.setSmallIcon(R.mipmap.ic_launcher)
// 4. 发送通知
notificationManager.notify(1, build.build())
渠道只能添加,不能修改,除非用户卸载 app,若是对通知渠道有变更请新添加渠道到系统,渠道在 application 中添加一次就好了,build 构建的时候指定通知渠道就行了
8.0 权限申请变动
8.0 之前,用户申请一个权限,那么会把该权限所在的组的权限都提供给用户,但是在 8.0 之后,你申请什么权限只给你什么权限,不再是一组都给你了,但是你之后要是再申请该组内的其他权限的话会直接给你该权限,不会再显示弹窗询问用户
网友评论