android 沉淀 - 版本变迁和适配

Android 一路走来十来年啦,从青涩懵懂的小女孩变成如今的大姑娘了,甚至很快就会做个大整容,虽然越变越好,但是带来的历史问题也是相当的棘手,屏幕需要适配说实话可以忍忍,但是历史版本的巨大差异我是真的忍不了,太不爽了...

5.0 之后版本的一些特性改变非常大,很多人都不是非常清楚,有必要门清


广播

从 7.0 API 24 开始,Google 为了优化系统环境开始逐步收紧广播的运行

  • 7.0 API 24 开始,静态注册的广播接收器无法监听网络变化:android.net.conn.CONNECTIVITY_CHANGE,以及 ACTION_NEW_PICTURE,ACTION_NEW_VIDEO 的广播
  • 8.0 版本对静态广播进一步加强了限制,除了下面的,其他的广播都不能用静态注册监听了,并且禁止静态注册的广播在 onReceive 方法中启动 Service。推荐大家代码注册广播
/ 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 的都是必须写的,不能空

  • name - 是这段路径在 Uri 中的代指,为了隐藏具体路径
  • path - 是该文件路径中的子路径,其实就是在这个文件夹里建子文件夹

一般不用写这么,我就写 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 之后,你申请什么权限只给你什么权限,不再是一组都给你了,但是你之后要是再申请该组内的其他权限的话会直接给你该权限,不会再显示弹窗询问用户


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,667评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,361评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,700评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,027评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,988评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,230评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,705评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,366评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,496评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,405评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,453评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,725评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,803评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,015评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,514评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,111评论 2 341

推荐阅读更多精彩内容