Android7.0适配总结

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/namehybin/article/details/78571964

一、权限更改

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。也就是说,对于应用间共享文件这块,Android N中做了强制性要求

来看一段代码

String cachePath = getApplicationContext().getExternalCacheDir().getPath();
File picFile = new File(cachePath, "test.jpg");
Uri picUri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, 100);

这是常见的打开系统相机拍照的代码,拍照成功后,照片会存储在picFile文件中。

这段代码在Android 7.0之前是没有任何问题,但是如果你尝试在7.0的系统上运行,会抛出FileUriExposedException异常。

使用FileProvider

FileProvider使用大概分为以下几个步骤:

  1. manifest中申明FileProvider
  2. res/xml中定义对外暴露的文件夹路径
  3. 生成content://类型的Uri
  4. 给Uri授予临时权限
  5. 使用Intent传递Uri
1.manifest中申明FileProvider:
<manifest>
  ...
  <application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.demo.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/paths" />
    </provider>
    ...
  </application>
</manifest>

android:name:provider你可以使用v4包提供的FileProvider,或者自定义的,只需要在name申明就好了,一般使用系统的就足够了。

android:authorities:类似schema,命名空间之类,后面会用到。

android:exported:false表示我们的provider不需要对外开放。

android:grantUriPermissions:申明为true,你才能获取临时共享权限。

2. res/xml中定义对外暴露的文件夹路径:

新建paths.xml,文件名随便起,后面会引用到。

<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <files-path name="my_images" path="images"/>
</paths>

name:一个引用字符串。

path:文件夹“相对路径”,完整路径取决于当前的标签类型。

path可以为空,表示指定目录下的所有文件、文件夹都可以被共享。

paths这个元素内可以包含以下一个或多个,具体如下:

<files-path name="name" path="path" />

物理路径相当于Context.getFilesDir() + /path/。

<cache-path name="name" path="path" />

物理路径相当于Context.getCacheDir() + /path/。

<external-path name="name" path="path" />

物理路径相当于Environment.getExternalStorageDirectory() + /path/。

<external-files-path name="name" path="path" />

物理路径相当于Context.getExternalFilesDir(String) + /path/。

<external-cache-path name="name" path="path" />

物理路径相当于Context.getExternalCacheDir() + /path/。

3.生成content://类型的Uri

我们通常通过File生成Uri的代码是这样:

File picFile = xxx;
Uri picUri = Uri.fromFile(picFile);

这样生成的Uri,路径格式为file://xxx。这种Uri是无法在App之间共享的,我们需要生成content://xxx类型的Uri,方法就是通过Context.getUriForFile来实现:

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.demo.fileprovider", newFile)
4.给Uri授予临时权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

FLAG_GRANT_READ_URI_PERMISSION:表示读取权限;
FLAG_GRANT_WRITE_URI_PERMISSION:表示写入权限。

5.使用Intent传递Uri

以开头的拍照代码作为示例,需要这样改写:

File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
// 授予目录临时共享权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 100);   

二、广播

于Android N后台的优化主要是关闭了三项系统广播:网络状态变更广播、拍照广播以及录像广播。

网络变化的广播(CONNECTIVITY_CHANGE),当网络发生变化时所有注册了隐式监听网络变化的app都会被启动。删除这些广播可以显著提升设备性能和用户体验。同样地,拍照广播和录视频广播(ACTION_NEW_PICTURE or ACTION_NEW_VIDEO)也会出现上述情况。

在Android N平台下即使在Manifest.xml清单文件中注册了 CONNECTIVITY_ACTION广播,在网络发生变化时也不会接收到任何的信息。但是正在前台运行的应用程序如果在主线程中通过Context.registerReceiver()动态注册了CONNECTIVITY_ACTION广播,该应用程序仍然可以接收到该广播。

三、分屏

Android N允许用户一次在屏幕中使用两个App,用户可以左右并排/上下摆放两个App来使用。用户还可以左右/上下拖拽中间的那个小白线来改变两个App的尺寸。
20170504149388867443363.png 20170504149388869827151.png

如何操作来进入分屏模式的:

  1. 点击右下角的方块,进入任务管理器,长按一个App的标题栏,将其拖入屏幕的高亮区域,这个App金进入了分屏模式。然后在任务管理器中选择另一个App,单击它使得这个App也进入分屏模式。
  2. 打开一个App,然后长按右下角的方块,此时已经打开的这个App将进入分屏模式。然后在屏幕上的任务管理器中选择另外一个App,单击它使得这个App也进入分屏模式。

分屏模式的生命周期

官方说法:在分屏模式下,用户最近操作、激活过的Activity将被系统视为topmost。而其他的Activity都属于paused状态,即使它是一个对用户可见的Activity。但是这些可见的处于paused状态的Activity将比那些不可见的处于paused状态的Activity得到更高优先级的响应。当用户在一个可见的paused状态的Activity上操作时,它将得到恢复resumed状态,并被系统视为topmost。而之前那个那个处于topmpst的Activity将变成paused状态。

那么这种可见的pause的状态将带来什么影响呢?

在分屏模式中,一个App可以在对用户可见的状态下进入paused状态,所以你的App在处理业务时,应该知道自己什么时候应该真正的暂停。例如一个视频播放器,如果进入了分屏模式,就不应该在onPaused()回调中暂停视频播放,而应该在onStop()回调中才暂停视频,然后在onStart回调中恢复视频播放。关于如果知道自己进入了分屏模式,在Android N的Activity类中,增加了一个void onMultiWindowChanged(boolean inMultiWindow)回调,所以我们可以在这个回调知道App是不是进入了分屏模式。

分屏时Activity的生命周期

  • 当前显示自己的应用页面,长按多任务键时出现分屏

    onConfigurationChanged()-> onMultiWindowModeChanged()-> onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()

  • 分屏时长按多任务键,全屏显示自己的应用时

    onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()-> onConfigurationChanged()-> onMultiWindowModeChanged()-> onResume()

如何设置App的分屏模式

怎样才能让App进入分屏模式呢?有下面这几个属性。

android:resizeableActivity

直接在AndroidManifest.xml中的或者标签下设置新的属性android:resizeableActivity=”true”。

设置了这个属性后,你的App/Activity就可以进入分屏模式了。

如果这个属性被设为false,那么你的App将无法进入分屏模式,如果你在打开这个App时,长按右下角的小方块,App将仍然处于全屏模式,系统会弹出Toast提示你无法进入分屏模式。这个属性在你target到Android N后,android:resizeableActivity的默认值就是true。

注意:假如你没有适配到Android N(targetSDKVersion < Android N),打包App时的compileSDKVersion < Android N,你的App也是可以支持分屏的!!!!原因在于:如果你的App没有设置 仅允许Activity竖屏/横屏,即没有设置android:screenOrientation=”XXX”属性时,运行Android N系统的设备还是可以将你的App分屏!! 但是这时候系统是不保证运行时的稳定性的,在进入分屏模式时,系统首先也会弹出Toast来提示你说明这个风险。

最新的Android N SDK中,Activity类中增加了下面的方法。

  • inMultiWindow():返回值为boolean,调用此方法可以知道App是否处于分屏模式。
  • onMultiWindowChanged(boolean inMultiWindow):当Activity进入或者退出分屏模式时,系统会回调这个方法来通知开发者。回调的参数inMultiWindow为boolean类型,如果inMultiWindow为true,表示Activity进入分屏模式;如果inMultiWindow为false,表示退出分屏模式。

支持拖拽

现在可以实现在两个分屏模式的Activity之间拖动内容。Android N Preview SDK中,View已经增加支持Activity之间拖动的API。具体的类和方法主要用到下面几个新的接口:

  • View.startDragAndDrop():View.startDrag() 的替代方法,需要传递View.DRAG_FLAG_GLOBAL来实现跨Activity拖拽。如果需要将URI权限传递给接收方Activity,还可以根据需要设置View.DRAG_FLAG_GLOBAL_URI_READ或者View.DRAG_FLAG_GLOBAL_URI_WRITE。
  • View.cancelDragAndDrop():由拖拽的发起方调用,取消当前进行中的拖拽。
  • View.updateDragShadow():由拖拽的发起方调用,可以给当前进行的拖拽设置阴影。
  • android.view.DropPermissions:接收方App所得到的权限列表。
  • Activity.requestDropPermissions():传递URI权限时,需要调用这个方法。传递的内容存储在DragEvent中的ClipData里。返回值为前面的android.view.DropPermissions。

20170505149395235143362.png

在FirstActivity中,发起拖拽。

imageView.setOnTouchListener(new View.OnTouchListener() {
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            /**
             *  构造一个ClipData,将需要传递的数据放在里面
             */
            ClipData.Item item = new ClipData.Item((CharSequence) view.getTag());
            String[] mimeTypes = {ClipDescription.MIMETYPE_TEXT_PLAIN};
            ClipData dragData = new ClipData(view.getTag().toString(), mimeTypes, item);
            View.DragShadowBuilder shadow = new View.DragShadowBuilder(imageView);
            /**
             * startDragAndDrop是Android N SDK中的新方法,替代了以前的startDrag,
             * flag需要设置为DRAG_FLAG_GLOBAL
             */
            view.startDragAndDrop(dragData, shadow, null, View.DRAG_FLAG_GLOBAL);
            return true;
        } else {
            return false;
        }
    }
});

在SecondActivity中,接收这个拖拽的结果,在ACTION_DROP事件中,把结果显示出来。

dropedText.setOnDragListener(new View.OnDragListener() {
    @Override
    public boolean onDrag(View view, DragEvent dragEvent) {
        switch (dragEvent.getAction()) {
            case DragEvent.ACTION_DRAG_STARTED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_STARTED");
                break;
            case DragEvent.ACTION_DRAG_ENTERED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENTERED");
                break;
            case DragEvent.ACTION_DRAG_EXITED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_EXITED");
                break;
            case DragEvent.ACTION_DRAG_LOCATION:
                break;
            case DragEvent.ACTION_DRAG_ENDED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENDED");
                break;
            case DragEvent.ACTION_DROP:
                Log.d(TAG, "ACTION_DROP event");
                //在这里显示接收到的结果
                dropedText.setText(dragEvent.getClipData().getItemAt(0).getText());
                break;
            default:
                break;
        }
        return true;
    }
});

分屏原理

分屏功能的实现主要依赖于ActivityManagerService与WindowManagerService这两个系统服务,它们都位于system_server进程中。该进程是Android系统中一个非常重要的系统进程。Framework中的很多服务都位于这个进程中。

整个Android的架构是CS的模型,应用程序是Client,而system_server进程就是对应的Server。

应用程序调用的很多API都会发送到system_server进程中对应的系统服务上进行处理,例如startActivity这个API,最终就是由ActivityManagerService进行处理。

而由于应用程序和system_server在各自独立的进程中运行,因此对于系统服务的请求需要通过Binder进行进程间通讯(IPC)来完成调用,以及调用结果的返回。

ActivityManagerService负责Activity管理。

对于应用中创建的每一个Activity,在ActivityManagerService中都会有一个与之对应的ActivityRecord,这个ActivityRecord记录了应用程序中的Activity的状态。ActivityManagerService会利用这个ActivityRecord作为标识,对应用程序中的Activity进程调度,例如生命周期的管理。

实际上,ActivityManagerService的职责远超出的它的名称,ActivityManagerService负责了所有四大组件(Activity,Service,BroadcastReceiver,ContentProvider)的管理,以及应用程序的进程管理。

WindowManagerService负责Window管理。包括:

窗口的创建和销毁
窗口的显示与隐藏
窗口的布局
窗口的Z-Order管理
焦点的管理
输入法和壁纸管理
等等 每一个Activity都会有一个自己的窗口,在WindowManagerService中便会有一个与之对应的WindowState。WindowManagerService以此标示应用程序中的窗口,并用这个WindowState来存储,查询和控制窗口的状态。

ActivityManagerService与WindowManagerService需要紧密配合在一起工作,因为无论是创建还是销毁Activity都牵涉到Actiivty对象和窗口对象的创建和销毁。这两者是既相互独立,又紧密关联在一起的。

Task和Stack

展开阅读全文

没有更多推荐了,返回首页