美文网首页Android收藏集Java技术问答
android 面试题 - 版本变迁和适配

android 面试题 - 版本变迁和适配

作者: 前行的乌龟 | 来源:发表于2019-06-14 21:54 被阅读58次

    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 的都是必须写的,不能空

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


    相关文章

      网友评论

        本文标题:android 面试题 - 版本变迁和适配

        本文链接:https://www.haomeiwen.com/subject/otcdxctx.html