Android7.0适配教程,心得

作者: CrazyCodeBoy | 来源:发表于2016-09-28 10:21 被阅读61299次

    Android7.0(Android N)适配教程,心得.png

    Android7.0发布已经有一个多月了,Android7.0在给用户带来一些新的特性的同时,也给开发者带来了新的挑战,这几天我将应用适配到Android7.0,其中也遇到了不少问题也踩了一些坑,在这里就把我在Android7.0适配上的一些心得分享给大家,让大家的应用能早一天跑在Android7.0上。

    权限更改

    随着Android版本越来越高,Android对隐私的保护力度也越来越大。从Android6.0引入的动态权限控制(Runtime Permissions)到Android7.0的“私有目录被限制访问”,“StrictMode API 政策”。这些更改在为用户带来更加安全的操作系统的同时也为开发者带来了一些新的任务。如何让你的APP能够适应这些改变而不是cash,是摆在每一位Android开发者身上的责任。

    目录被限制访问

    一直以来,在目录及文件的访问保护方面iOS做的是很到位的,如:iOS的沙箱机制。但,Android在这方面的保护就有些偏弱了,在Android中应用可以读写手机存储中任何一个目录及文件,这也带来了很多的安全问题。现在Android也在着力解决这一问题。

    在Android7.0中为了提高私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。对于这个权限的更改开发者需要留意一下改变:

    应对策略:这项权限的变更将意味着你无法通过File API访问手机存储上的数据了,基于File API的一些文件浏览器等也将受到很大的影响,看到这大家是不是惊呆了呢,不过迄今为止,这种限制尚不能完全执行。 应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。 但是,Android官方强烈反对放宽私有目录的权限。可以看出收起对私有文件的访问权限是Android将来发展的趋势。

    • 给其他应用传递 file:// URI 类型的Uri,可能会导致接受者无法访问该路径。 因此,在Android7.0中尝试传递 file:// URI 会触发 FileUriExposedException。

    应对策略:大家可以通过使用FileProvider来解决这一问题。

    应对策略:大家可以通过[ContentResolver.openFileDescriptor()](https://developer.android.com/reference/android/content/ContentResolver.html#openFileDescriptor(android.net.Uri, java.lang.String))来访问由 DownloadManager 公开的文件。

    应用间共享文件

    在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照,或裁切照片

    应对策略:若要在应用间共享文件,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider类。 如需有关权限和共享文件的更多信息,请参阅共享文件。

    在Android7.0上调用系统相机拍照,裁切照片

    调用系统相机拍照

    在Android7.0之前,如果你想调用系统相机拍照可以通过以下代码来进行:

    File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists())file.getParentFile().mkdirs();
    Uri imageUri = Uri.fromFile(file);
    Intent intent = new Intent();
    intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//将拍取的照片保存到指定URI
    startActivityForResult(intent,1006);
    
    Android7.0拍照.pngAndroid7.0拍照.png

    在Android7.0上使用上述方式调用系统相拍照会抛出如下异常:

    android.os.FileUriExposedException: file:////storage/emulated/0/temp/1474956193735.jpg exposed beyond app through Intent.getData()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
    at android.net.Uri.checkFileUriExposed(Uri.java:2346)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
    at android.app.Activity.startActivityForResult(Activity.java:4223)
    ...
    at android.app.Activity.startActivityForResult(Activity.java:4182)
    
    Android7.0拍照闪退.pngAndroid7.0拍照闪退.png

    这是由于Android7.0执行了“StrictMode API 政策禁”的原因,不过小伙伴们不用担心,上文讲到了可以用FileProvider来解决这一问题,
    现在我们就来一步一步的解决这个问题。

    使用FileProvider

    使用FileProvider的大致步骤如下:
    第一步:在manifest清单文件中注册provider

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.jph.takephoto.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则会报安全异常。grantUriPermissions:true,表示授予 URI 临时访问权限。

    第二步:指定共享的目录

    为了指定共享的目录我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <paths>
            <external-path path="" name="camera_photos" />
        </paths>
    </resources>
    
    • <files-path/>代表的根目录: Context.getFilesDir()
    • <external-path/>代表的根目录: Environment.getExternalStorageDirectory()
    • <cache-path/>代表的根目录: getCacheDir()

    心得:上述代码中path="",是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path="pictures"
    那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。

    第三步:使用FileProvider

    上述准备工作做完之后,现在我们就可以使用FileProvider了。
    还是以调用系统相机拍照为例,我们需要将上述拍照代码修改为如下:

    File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists())file.getParentFile().mkdirs();
    Uri imageUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", file);//通过FileProvider创建一个content类型的Uri
    Intent intent = new Intent();
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//将拍取的照片保存到指定URI
    startActivityForResult(intent,1006);
    

    上述代码中主要有两处改变:

    1. 将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri。
    2. 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。

    心得:上述代码通过FileProviderUri getUriForFile (Context context, String authority, File file)
    静态方法来获取Uri,该方法中authority参数就是清单文件中注册provider的android:authorities="com.jph.takephoto.fileprovider"
    对Web服务器如tomcat,IIS比较熟悉的小伙伴,都只知道为了网站内容的安全和高效,Web服务器都支持为网站内容设置一个虚拟目录,其实FileProvider也有异曲同工之处。

    getUriForFile方法获取的Uri打印出来如下:

    content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg`。  
    

    其中camera_photos就是file_paths.xml中paths的name。

    因为上述指定的path为path="",所以content://com.jph.takephoto.fileprovider/camera_photos/代表的真实路径就是根目录,即:/storage/emulated/0/
    content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg代表的真实路径是:/storage/emulated/0/temp/1474960080319.jpg

    另外,推荐大家使用开源工具库TakePhoto
    TakePhoto是一款在Android设备上获取照片(拍照或从相册、文件中选择)、裁剪图片、压缩图片的开源工具库。

    裁切照片

    在Android7.0之前,你可以通过如下方法来裁切照片:

    File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists())file.getParentFile().mkdirs();
    Uri outputUri = Uri.fromFile(file);
    Uri imageUri=Uri.fromFile(new File("/storage/emulated/0/temp/1474960080319.jpg"));
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(imageUri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    intent.putExtra("scale", true);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    intent.putExtra("noFaceDetection", true); // no face detection
    startActivityForResult(intent,1008);
    

    和拍照一样,上述代码在Android7.0上同样会引起android.os.FileUriExposedException异常,解决办法就是上文说说的使用FileProvider

    然后,将上述代码改为如下即可:

    File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists())file.getParentFile().mkdirs();
    Uri outputUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider",file);
    Uri imageUri=FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", new File("/storage/emulated/0/temp/1474960080319.jpg");//通过FileProvider创建一个content类型的Uri
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(imageUri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    intent.putExtra("scale", true);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    intent.putExtra("noFaceDetection", true); // no face detection
    startActivityForResult(intent,1008);
    

    另外,裁切照片推荐大家使用开源工具库TakePhoto
    TakePhoto是一款在Android设备上获取照片(拍照或从相册、文件中选择)、裁剪图片、压缩图片的开源工具库。

    电池和内存

    Android 6.0(API 级别 23)引入了低电耗模式,Android7.0在电池和内存上又做了进一步优化,
    来减少Android应用对电量的消耗以及对内存的占用。这些优化所带来的一些规则的变更可能会影响你的应用访问系统资源,以及你的系统通过特定隐式 Intent 与其他应用互动的方式。
    所以开发人员需要特别注意这些改变。

    低电耗模式

    在低电耗模式下,当用户设备未插接电源、处于静止状态且屏幕关闭时,该模式会推迟 CPU 和网络活动,从而延长电池寿命。
    Android7.0通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持式设备装在口袋里)时应用部分 CPU 和网络限制,进一步增强了低电耗模式。

    也就是说,Android7.0会在手机屏幕关闭的状态下,限时应用对CPU以及网络的使用。

    具体规则如下:

    1. 当设备处于充电状态且屏幕已关闭一定时间后,设备会进入低电耗模式并应用第一部分限制: 关闭应用网络访问、推迟作业和同步。
    2. 如果进入低电耗模式后设备处于静止状态达到一定时间,系统则会对 PowerManager.WakeLockAlarmManager
      闹铃、GPS 和 Wi-Fi 扫描应用余下的低电耗模式限制。 无论是应用部分还是全部低电耗模式限制,系统都会唤醒设备以提供简短的维护时间窗口,在此窗口期间,应用程序可以访问网络并执行任何被推迟的作业/同步。

    后台优化

    小伙伴们都知道在Android中有一些隐式广播,使用这些隐式广播可以做一些特定的功能,如,当手机网络变成WiFi时自动下载更新包等。
    但,这些隐式广播会在后台频繁启动已注册侦听这些广播的应用,从而带来很大的电量消耗,为缓解这一问题来提升设备性能和用户体验,在Android 7.0中删除了三项隐式广播,以帮助优化内存使用和电量消耗。

    Android 7.0 应用了以下优化措施:

    • 在 Android 7.0上 应用不会收到 CONNECTIVITY_ACTION 广播,即使你在manifest清单文件中设置了请求接受这些事件的通知。 但,在前台运行的应用如果使用BroadcastReceiver 请求接收通知,则仍可以在主线程中侦听 CONNECTIVITY_CHANGE。
    • 在 Android 7.0上应用无法发送或接收 ACTION_NEW_PICTUREACTION_NEW_VIDEO 类型的广播。

    应对策略:Android 框架提供多个解决方案来缓解对这些隐式广播的需求。 例如,JobScheduler API
    提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。 您甚至可以使用 JobScheduler API 来适应内容提供程序变化。

    另外,大家如果想了解更多关于后台的优化可查阅后台优化

    移动设备会经历频繁的连接变更,例如在 Wi-Fi 和移动数据之间切换时。 目前,可以通过在应用清单中注册一个接收器来侦听隐式 CONNECTIVITY_ACTION 广播,
    让应用能够监控这些变更。 由于很多应用会注册接收此广播,因此单次网络切换即会导致所有应用被唤醒并同时处理此广播。

    以上是,我在Android7.0上适配上的一些心得,小伙伴们如果有遇到问题可以在下方留言。

    最后

    既然来了,留下个喜欢再走吧,鼓励我继续创作(_)∠※

    如果喜欢我的文章,那就关注我的博客吧,让我们一起做朋友~~

    戳这里,加关注哦:

    微博:第一时间获取推送
    个人博客:干货文章都在这里哦
    GitHub:我的开源项目

    相关文章

      网友评论

      • 0db6241915fd:不错,比较容易看懂
      • 失落_70dc:Android camera写的动画7.0不能用
      • 40f614d9bb95:楼主,有一个问题困扰我很多天,还是不能排查出来,能不能帮我看下。

        public void startPhotoZoom(File file) {
        Intent intent = new Intent("com.android.camera.action.CROP");
        Uri imageUri=FileProvider.getUriForFile(this, "com.xh.google.provider.NotePad", file);
        Uri outputUri = Uri.fromFile(file);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        intent.setDataAndType(imageUri, "image/*");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //弹窗提示权限
        intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
        Log.i("outputUri:", outputUri + "");
        } else {
        intent.setDataAndType(Uri.fromFile(file), "image/*");
        intent.putExtra(MediaStore.EXTRA_OUTPUT, file);
        }
        intent.putExtra("crop", "true");// crop=true 有这句才能出来最后的裁剪页面.
        intent.putExtra("scale", true);
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra("outputFormat", "JPEG");
        startActivityForResult(intent, GlobalParams.REQUEST_CODE_CUT_IMG);
        }

        我代码是这样写的,然后当我裁剪完毕后最后的回调事件REQUEST_CODE_CUT_IMG中是执行不到的,请问是什么原因吗?
      • 蜗牛1:只是下载的话7.0有没有什么问题
      • 紫豪:这是我见过。。。在简书上喜欢数最高的技术文:joy:
      • 张憨憨:哥们我在取出存取路径和存取路径 照片文件名就少了个1怎么回事
        05-21 09:14:41.170 10604-10604/? E/ContentValues: 保存在本地图片路劲: content://zhq.com.a70fileprovide/camera-photos/23456.jpg
        05-21 09:14:46.817 10604-10604/? E/ContentValues: 保存在本地图片取出路劲: /storage/emulated/0/123456.jpg
      • 509ec1f9f4f6:截图那段代码,outputuri有问题
      • 取帅气的倪称:楼主,我想问一下,7.0下载后如何快速的刷新文件夹?之前的版本发送广播并没有效果
      • 丶Hgf:非常赞!之前就有注意到有这方面的问题了,但还是第一次看的这么详细
      • 酥脆海苔饼干:针对SecurityException异常,请问作者暂时有解决办法吗?
        MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 两个权限应变为什么?
      • 年少常慕楼外楼:那个图片共享的路径我获取到了,但是如何把图片显示出来呢?我用img.setImageURI(imgUri);去显示的共享文件的uri所指向的图片
      • PiNoLa:老板要我2天赶出来!!!想想都可怕
      • 冰冰的冻结:模拟器下测试 拍照截图时 ,保存报安全异常
      • 冰冰的冻结:拍照 截屏 保存后就崩溃

        java.lang.SecurityException: Permission Denial: writing android.support.v4.content.FileProvider uri content://com.tianshaokai.demo.fileprovider/camera_photos/temp/1480473262410.jpg from pid=2981, uid=10041 requires the provider be exported, or grantUriPermission()

        这是错误日志
      • 冰冰的冻结:<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.jph.takephoto.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
        </provider>


        android:authorities="com.jph.takephoto.fileprovider" 这个名字是不是可以随便写呢,还是说必须这么写呢 我改成了自己的包名,改不改这个地方一直报 空指针 不知道怎么回事
      • 黑丫山上小旋风:如何让你的APP能够适应这些改变而不是cash
        crash 吧~
        学习一下
      • ruby玉:作者大大,有没有遇到跳转到裁剪显示缩略图的界面,一直显示的是第一次的缩略图,而不是选择的那张
        CrazyCodeBoy:@09b858680734 不太清楚你的意思,看这里有没有你要找的答案https://github.com/crazycodeboy/TakePhoto/issues/51
      • 伤zAi蔓延:照片 截取输出的outputUri, 只能使用 Uri.fromFile,不能用FileProvider.getUriForFile,不然会报错的。
        0b9a6b25fbfd:@伤zAi蔓延 :smiley: 果然如此啊,这算bug么?
      • 刘涤生:你好 我按照上述的代码在Android 7.0进行裁剪 保存的时候崩溃,弄了好久都不知道原因,请指教!

        我的代码是

        File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
        if (!file.getParentFile().exists())file.getParentFile().mkdirs();
        outputUri = FileProvider.getUriForFile(this, "com.michael.materialdesign.provider",file);
        Uri imageUri=FileProvider.getUriForFile(this, "com.michael.materialdesign.provider", new File("/storage/emulated/0/temp/1476865100115.jpg"));//通过FileProvider创建一个content类型的Uri
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(imageUri, "image/*");
        intent.putExtra("crop", "true");
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra("scale", true);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        intent.putExtra("noFaceDetection", true); // no face detection
        startActivityForResult(intent,1008);

        异常为:

        Writing exception to parcel
        java.lang.SecurityException: Permission Denial: writing android.support.v4.content.FileProvider uri content://com.michael.materialdesign.provider/camera_photos/temp/1476865428597.jpg from pid=14849, uid=10037 requires the provider be exported, or grantUriPermission()
        at android.content.ContentProvider.enforceWritePermissionInner(ContentProvider.java:682)
        at android.content.ContentProvider$Transport.enforceWritePermission(ContentProvider.java:497)
        at android.content.ContentProvid
        伤zAi蔓延:@Michael727 没事,相互学习
        刘涤生:@伤zAi蔓延 真是这样啊 多谢
        伤zAi蔓延:@Michael727 照片 截取输出的outputUri, 只能使用 Uri.fromFile,不能用FileProvider.getUriForFile
      • lchad:如果path和那么都指定为Zhuangbility,Uri内的路径是对的,但是仍然无法找到文件
      • lchad:你好,我在编写xml的时候,指定path为空,name为我的应用的文件夹名(Zhuangbility),但是断点之后Uri里面的路径是
        content://com.liuchad.zhuangbility.fileprovider/Zhuangbility/Zhuangbility/some_picture.jpeg
        导致接受的应用(QQ)找不到这个文件,提示文件不存在。
        不知你有没有遇到这样的状况?
      • 5897f4c57446:赞一个, 先mark一下 :smile:
      • fearless雷少:发现7.0下面基本的window属性也有变化,7.0之前popupwindow showAsDropDown,match_parent 是可以正常显示的,而在7.0上面,直接没用而是全屏,这个解决办法,必须动态计算高度 才能正常显示,高度screenHeight-anchor's Height。
        Sunny旋律:@fearless雷少 你好,我最近正好遇到这个问题,能不能说一下具体做法呢
      • 49666dc85b62:不错,写的棒
      • ZapFive69:写得很不错 mark
      • EitanLiu:我能这么理解吗,只是禁止intent传递file路径uri,文件还是能正常读写
      • sendtion:好迅速
      • 32576c200ae1:先mark
      • 广成de微博:如何让你的APP能够适应这些改变而不是cash,是摆在每一位Android开发者身上的责任。

        ...我要 cash... :smiley:
        北疆_:你要cash,程序不能crash
        snowdream:应该是crash,^_^
        暮雨沉沦:@广成de微博 cash?
      • C_Sev:卧槽
      • 迷途小书童nb:<external-path path="Pictures" name="camera_photos" />这样获取的uri路径,我6p上测试的结果是/camera_photos ,而不是sd卡的路径,可能系统有bug。
      • 88c1701043a0:感谢分享!

      本文标题:Android7.0适配教程,心得

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