当前位置: 首页 > 科技观察

老司机光飙车技术:安卓7.0适配体验

时间:2023-03-18 18:23:59 科技观察

安卓7.0已经发布一个多月了。Android7.0在为用户带来一些新特性的同时,也为开发者带来了新特性。挑战一下,这几天把应用适配到Android7.0,遇到了很多问题,也踩了一些坑。在这里我将我在Android7.0适配上的一些经验分享给大家,让大家的应用早一天在Android7.0上运行起来。权限变化随着安卓版本越来越高,安卓对隐私的保护也越来越强。从Android6.0引入的动态权限控制(RuntimePermissions)到Android7.0的“私有目录被限制访问”和“StrictModeAPI策略”。这些变化在为用户带来更安全的操作系统的同时,也为开发人员带来了一些新任务。如何让你的APP适应这些变化而不是变现,是每一个Android开发者的责任。对目录的访问受到限制。长期以来,iOS在保护目录和文件方面做得很好,比如iOS的沙盒机制。但是,Android在这方面的保护有些薄弱。在Android中,应用程序可以读写手机存储中的任意目录和文件,这也带来了很多安全问题。现在安卓也在努力解决这个问题。Android7.0为了提高隐私文件的安全性,将限制AndroidN以上应用程序访问隐私目录。开发者需要注意该权限的变化:私有文件的文件权限不再委托给所有应用程序,使用MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE进行操作会触发SecurityException。对策:此次权限变更将意味着您将无法再通过FileAPI访问手机中存储的数据,部分基于FileAPI的文件浏览器也会受到较大影响。看到这里你是不是很震惊?嗯,但是到目前为止,这个限制还没有完全执行。应用程序仍然可以使用本机API或文件API修改其私有目录权限。但是Android官方强烈反对放宽私有目录的权限。可见,放开对私有文件的访问是Android未来的发展趋势。将file://URI类型的Uri传递给其他应用程序可能会导致接收方无法访问该路径。因此,在Android7.0中尝试传递file://URI将触发FileUriExposedException。应对策略:可以使用FileProvider来解决这个问题。DownloadManager不再按文件名共享私人存储的文件。COLUMN_LOCAL_FILENAME在Android7.0中被标记为deprecated,旧版本的应用在访问COLUMN_LOCAL_FILENAME时可能存在无法访问的路径。以AndroidN或更高版本为目标平台的应用在尝试访问COLUMN_LOCAL_FILENAME时将抛出SecurityException。解决方法:您可以通过ContentResolver.openFileDescriptor()访问DownloadManager公开的文件。在应用程序之间共享文件在Android7.0上,Android框架强制执行StrictModeAPI策略以禁止在应用程序外部公开file://URI。如果包含file://URI类型的Intent离开您的应用程序,应用程序将失败并抛出FileUriExposedException异常,例如调用系统相机拍照,或裁剪图片。对策:要在应用程序之间共享文件,可以发送一个类型为content://URI的Uri并授予对该URI的临时访问权限。执行此授权的最简单方法是使用FileProvider类。有关权限和共享文件的更多信息,请参阅共享文件。Android7.0调用系统相机拍照,裁剪照片调用系统相机拍照在Android7.0之前,如果要调用系统相机拍照,可以使用如下代码:Filefile=newFile(Environment.getExternalStorageDirectory(),"/temp/"+System.currentTimeMillis()+".jpg");if(!file.getParentFile().exists())file.getParentFile().mkdirs();UriimageUri=Uri.fromFile(file);Intentintent=newIntent();intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action拍照intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);//将抓拍到的照片保存到指定URIstartActivityForResult(意图,1006);在Android7.0上使用上述方法调用系统拍照会抛出如下异常:android.os.FileUriExposedException:file:////storage/emulated/0/temp/1474956193735.jpgexposedbeyondappthroughIntent.getData()os.StrictMode.onFileUriExposed(StrictMode.java:1799)atandroid.net.Uri.checkFileUriExposed(Uri.java:2346)atandroid.content.Intent.prepareToLeaveProcess(Intent.java:8933)atandroid.content.Intent.prepareToLeaveProcess(Intent.java:8894)atandroid.app.Instrumentation.execStartActivity(Instrumentation.java:1517)atandroid.app.Activity.startActivityForResult(Activity.java:4223)...atandroid.app.Activity.startActivityForResult(Activity.java:4182)这是因为Android7.0实现了“StrictModeAPI策略禁止”,不过小伙伴们,别着急,我们上面提到了可以使用FileProvider来解决这个问题。下面我们一步步来解决这个问题。:name="android.support.v4.content.FileProvider"android:authorities="com.jph.takephoto.fileprovider"android:grantUriPermissions="true"android:exported="false">经验:exported:要求必须为false,为true会报安全异常。grantUriPermissions:true,表示授予URI临时访问权限。第二步:指定共享目录为了指定共享目录,我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”的文件(名字可以任意,只要是manifest中注册的provider引用的资源必须一致),资源文件内容如下:表示根目录:Context.getFilesDir()表示根目录:Environment.getExternalStorageDirectory()表示根目录:getCacheDir()经验:上面代码中的path=""有一个特殊的含义是的,它编码的是根目录,也就是说你可以将根目录及其子目录下的任何文件共享给其他应用程序。如果你设置path为path="pictures",那么它代表的是根目录目录下的图片(eg:/storage/emulated/0/pictures),如果你在pictures目录范围之外与其他应用共享文件,这是不可接受的。第三步:使用FileProvider上面的准备工作做好之后,我们就可以使用FileProvider了。仍然以调用系统相机拍照为例,我们需要将上面的拍照代码修改为:Filefile=newFile(Environment.getExternalStorageDirectory(),"/temp/"+System.currentTimeMillis()+".jpg");if(!file.getParentFile().exists())file.getParentFile().mkdirs();UriimageUri=FileProvider.getUriForFile(context,"com.jph.takephoto.fileprovider",file);//创建acontenttypethroughFileProviderUriIntentintent=newIntent();intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这句临时授权目标应用程序到Uri代表的文件intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Actiontotakeaphotointent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);//将拍摄的照片保存到指定的URIstartActivityForResult(intent,1006);上述代码主要有两处变化:将scheme类型为file的Uri改为FileProvider创建的类型为content的Uri。添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);临时授权目标应用程序访问Uri表示的文件。经验:以上代码通过FileProvider的UrigetUriForFile(Context上下文,String权限,File文件)静态方法获取Uri。该方法中的authority参数为manifest文件中注册的providerandroid:authorities="com.jph.takephoto.fileprovider"。熟悉tomcat、IIS等web服务器的朋友只知道,为了网站内容的安全和效率,web服务器支持为网站内容设置虚拟目录。其实FileProvider也有同样的目的。打印getUriForFile方法得到的Uri如下:content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg`。其中camera_photos是file_paths.xml中的路径名称。因为上面指定的路径是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之前,可以通过以下方法裁剪照片:Filefile=newFile(Environment.getExternalStorageDirectory(),"/temp/"+System.currentTimeMillis()+.jpg");if(!file.getParentFile().exists())file.getParentFile().mkdirs();UrioutputUri=Uri.fromFile(file);UriimageUri=Uri.fromFile(newFile("/storage/emulated/0/temp/1474960080319.jpg"));Intentintent=newIntent("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);//nofacedetectionstartActivityForResult(intent,1008);和拍照一样,上面的代码在Android7.0上也会引发android.os.FileUriExposedException,解决办法就是使用上面提到的FileProvider。然后,将上面的代码改为:Filefile=newFile(Environment.getExternalStorageDirectory(),"/temp/"+System.currentTimeMillis()+".jpg");if(!file.getParentFile().exists())file.getParentFile().mkdirs();UrioutputUri=FileProvider.getUriForFile(上下文,"com.jph.takephoto.fileprovider",文件);UriimageUri=FileProvider.getUriForFile(上下文,"com.jph.takephoto.fileprovider",newFile("/storage/emulated/0/temp/1474960080319.jpg");//通过FileProviderIntentintent=newIntent("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);//nofacedetectionstartActivityForResult(intent,1008);另外,推荐大家使用开源工具库TakePhoto进行照片裁剪。裁剪图片、压缩图片、电池和内存的开源工具库Android6.0(APIlevel23)引入了低功耗模式,Android7.0进一步优化了电池和内存,降低Android应用耗电量和内存占用.这些优化引入的一些规则更改可能会影响您的应用程序如何访问系统资源以及您的系统如何通过某些隐式意图与其他应用程序交互。所以开发者需要特别注意这些变化。Doze模式在Doze模式下,该模式会延迟CPU和网络活动,以在用户设备未插电、不活动且屏幕关闭时延长电池寿命。Android7.0通过在设备未插电且屏幕关闭但不一定处于静止状态(例如,当用户在口袋里携带手持设备外出走动时)时应用部分CPU和网络节流来进一步增强低电量。消费模式。也就是说,Android7.0会在手机屏幕关闭的情况下,在限定时间内限制应用的CPU和网络使用。具体规则如下:当设备正在充电并且屏幕已关闭一定时间后,设备进入Doze并应用第一部分限制:关闭应用程序网络访问、推迟作业和同步。如果设备在进入Doze后有一段时间处于非活动状态,剩余的Doze限制将应用于PowerManager.WakeLock、AlarmManager警报、GPS和Wi-Fi扫描。无论部分或所有Doze限制是否适用,系统都会唤醒设备以提供一个简短的维护窗口,在此期间应用程序可以访问网络并执行任何延迟的作业/同步。后台优化的朋友都知道Android中有一些隐式广播,利用这些隐式广播可以完成一些特定的功能,比如当手机网络变成WiFi时自动下载更新包。然而,这些隐式广播会在后台频繁启动已经注册收听这些广播的应用程序,这会造成大量的功耗。为了缓解这个问题,提高设备性能和用户体验,Android7.0中删除了三项。隐式广播有助于优化内存使用和功耗。Android7.0应用了以下优化:在Android7.0上,应用程序不会收到CONNECTIVITY_ACTION广播,即使您在清单中请求这些事件的通知。但是,如果在前台运行的应用程序使用BroadcastReceiver请求通知,它们仍然可以在主线程上监听CONNECTIVITY_CHANGE。Android7.0上的应用无法发送或接收类型为ACTION_NEW_PICTURE或ACTION_NEW_VIDEO的广播。应对策略:Android框架提供了几种解决方案来减轻对这些隐式广播的需求。例如,JobSchedulerAPI提供了一种健壮可靠的机制来安排在满足指定条件(例如连接到无限流量网络)时执行的网络操作。您甚至可以使用JobSchedulerAPI来适应内容提供者的变化。另外,如果想了解更多关于后台优化的知识,可以参考后台优化。移动设备会经历频繁的连接变化,例如在Wi-Fi和移动数据之间切换。目前,应用程序可以通过在应用程序清单中注册接收器来监听隐式CONNECTIVITY_ACTION广播来监视这些更改。由于许多应用程序注册接收此广播,单个网络切换将导致所有应用程序同时唤醒并处理此广播。