[转载][Android]模拟器现已支持 AMD 处理器和 Hyper-V

原创: Google Play 谷歌开发者 Yesterday
作者:Jamal Eason, Android 产品经理

自两年前谷歌对 Android 模拟器进行重大更新以来,我们一直致力于开发出一款运行速度快、功能全面的模拟器,帮助您为用户打造卓越应用体验。Android 模拟器现已成为 Android Studio 中最受欢迎的设备 —— 使用量为 Android 实体设备的两倍以上。很高兴看到 Android 模拟器一路以来陪伴各位开发者共同成长,但是旅程才刚刚开始,我们还可能做得更好。

模拟器速度一直是 Android Studio 团队重点攻克的难题之一:在之前的几个版本中,我们相继加入快速启动和模拟器快照功能,让开发者能够在 2 秒内快速启动模拟器并恢复之前的会话。迄今为止,Android 模拟器已能够在 macOS® 以及 Linux 设备上流畅运行,但对 Windows 或者 Hyper-V 平台用户而言并非如此,Android 模拟器支持只在英特尔处理器上提供的硬件加速增强。通过在 Android 模拟器添加 AMD 处理器以及 Hyper-V 虚拟机的支持,我们在本次版本更新中顺利解决了开发者社区里这两项存在已久的用户请求。

>> 模拟器快照功能链接:
https://developer.android.google.cn/studio/run/emulator#snapshots

今天,您就可以下载最新版本 Android 模拟器,在搭载 AMD 处理器的电脑上运行 Android x86 虚拟设备。本次重要更新同时也会让应用开发者们更容易接入 Android 模拟器,不仅不会受到此前的软件模拟上的限制,还会获得硬件加速性能支持。而且,对于那些希望利用 Hyper-V 运行自己本地应用后端的用户来说,现在的 Android 模拟器也可以和 Windows 10 上 Hyper-V 支持的其他应用兼容运行。

>> 最新版本 Android 模拟器链接:
https://developer.android.google.cn/studio/run/emulator#install

得益于新的 Windows 虚拟化管理平台 (WHPX) API 以及微软开源项目上作出的努力,更多 Android 应用开发者能够体验到模拟器在速度以及功能方面的显著改进。

>> Windows 虚拟化管理平台链接:
https://docs.microsoft.com/en-us/virtualization/api/

以上技术支持最早在 Android 模拟器 v27.3.8 (金丝雀版本) 中试行,而现在我们将这些预览版特性 (AMD 处理器以及 Hyper-V 支持) 推广至稳定版,希望获得更多反馈。此外,我们还提升了模拟器快照的加载速度,让使用英特尔硬件加速执行管理器(HAXM) 的开发者将获得更好体验。

如何使用

Linux系统

若您正在使用 Linux 进行 Android 应用开发,Android 模拟器将继续使用原生 KVM 虚拟技术管理工具为英特尔以及 AMD 设备提供高速、高性能的虚拟化解决方案。Android 模拟器 v27.3.8 新增加快照用户界面,并在性能、稳定性和资源利用方面的表现更为出色。

>> KVM 虚拟技术管理工具链接:
https://www.linux-kvm.org/page/Main_Page

macOS系统

若系统为 OS X v10.10 Yosemite 或更高版本,Android 模拟器在默认情况下继续使用内置 Hypervisor.Framework,且在框架无法启动的情况下 (如系统为 OS X v10.9 或更低版本),转用英特尔硬件加速执行管理器 (HAXM)。在升级至最新 macOS 版模拟器之后,您可以使用新增的快照用户界面,并享受到性能更好、稳定性更强的 Android 模拟器。

>> Hypervisor.Framework 链接:
https://developer.apple.com/documentation/hypervisor

微软 Windows 系统

对于使用英特尔 x86 处理器的设备来说,默认情况下 Android 模拟器将继续使用硬件加速执行管理器技术 (Intel HAXM)。该技术是英特尔开发的一款较为成熟的开源虚拟化技术解决方案。此外,由于英特尔在创新研发方面的持续投入,HAXM 依旧是目前市面上最快的 Android 模拟器加速技术。请前往 Android SDK 管理器页面查看更新,下载最新版本英特尔 HAXM v7.2.0。

若您的设备使用的是 AMD 处理器,需同时满足以下条件:

AMD 处理器 —— 推荐使用 AMD 锐龙系列处理器;

Android Studio 3.2 Beta 或更高版本,点击前往 Android Studio 预览版下载页面;

Android 模拟器 v27.3.8 +,点击前往 Android Studio SDK 管理器页面下载;

x86 Android 虚拟设备 (AVD),创建虚拟设备;

Windows 10 Version 1803 四月更新版;

在 Windows 功能菜单中勾选 “Windows Hypervisor Platform”

>> Android Studio 预览版链接:
https://developer.android.google.cn/studio/preview/

>> Android Studio SDK 管理器链接:
https://developer.android.google.cn/studio/intro/update#sdk-manager

>> 创建 AVD 链接:
https://developer.android.google.cn/studio/run/managing-avds#createavd

如果您想在配有英特尔处理器的设备上并行运行 Hyper-V 与 Android 模拟器,请根据上文指示更新 Android Studio 与 Android 模拟器,同时:

在 Windows 功能菜单中勾选 “Hyper-V” —— 仅支持 Windows 10 专业版、教育版与企业版

英特尔处理器:确保您的 Intel Core 处理器支持虚拟化技术 (VT-x)、扩展页表 (ETP) 以及不受限客户机 (UG) 功能;并在 BIOS 管理设置中启用 VT-x 虚拟化选项。

阅读文档 (https://developer.android.google.cn/studio/run/emulator-acceleration),了解更多安装技巧以及错误排查细节。

概括来说:若您的 Windows 设备使用英特尔处理器,Android 模拟器将继续使用英特尔 HAXM 技术 —— 它的速度更快,同时也是我们的推荐配置;若设备使用 AMD 处理器或 Hyper-V 虚拟机进行开发,您也不妨尝试一下新的 Android 模拟器,相信它会给您带去不少惊喜。

[Android][targetSdkVersion]应用程序更新的指定目标必须是Android8.0(API等级26)


最近一次更新APP时,Google Play提示了这个信息。。。
历史的欠账总归要补上,貌似有点麻烦。。。
一步一个脚印,记录targetSdkVersion从21升级到26遇到的问题吧。。。
官方SDK版本更新总览:https://developer.android.com/about/versions/marshmallow/android-6.0-changes#behavior-runtime-permissions
官方TargetSDK说明:https://developer.android.com/distribute/best-practices/develop/target-sdk
中文PDF版:Google Play 目标 API 等级(targetSdkVersion)重要变更要求  _  Android Developers

1、Runtime Permission(在运行时请求权限
这是 Android 6.0(API 级别 23)引进的,运行时态才去检查权限,而不是安装的时候。权限划分为多个保护级别,第三方应用受到下面三个级别的影响:normal, signature, dangerous.

Normal permissions

权限被声明为Normal级别,任何应用都可以申请,在安装应用时,不会直接提示给用户,点击全部才会展示。
截至Android8.1,下面的权限都是正常许可:

ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MANAGE_OWN_CALLS
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_COMPANION_RUN_IN_BACKGROUND
REQUEST_COMPANION_USE_DATA_IN_BACKGROUND
REQUEST_DELETE_PACKAGES
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
SET_ALARM
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS

Signature permissions

权限被声明为Signature级别,只有和该apk(定义了这个权限的apk)用相同的私钥签名的应用才可以申请该权限。

Dangerous permissions

权限被声明为Dangerous级别,任何应用都可以申请,在安装应用时,会直接提示给用户。

android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR
android.permission.CAMERA
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION
android.permission.RECORD_AUDIO
android.permission.READ_PHONE_STATE
android.permission.READ_PHONE_NUMBERS
android.permission.CALL_PHONE
android.permission.ANSWER_PHONE_CALLS
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
android.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS
android.permission.BODY_SENSORS
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE
android.permission.SYSTEM_ALERT_WINDOW

在manifest文件中查找需要添加权限获取的类型,目前项目中使用了下面几个权限分组:Camera、Storage、Location、Phone。
Camera权限分组:比较简单,在扫描二维码的activity启动之前验证权限即可,代码异步回调调用。
Storage权限分组:需要根据调用的文件路径来区分,如下图所示,外部存储和内部存储的区别

也就是说访问 Environment.getExternalStorageDirectory类型的,都需要加上权限判断(还包括getExternalStoragePublicDirectory函数指定的参数、各种公共目录如DCIM、Music、Pictures、Movies、Downloads、Documents等等)。

2、FileUriExposedException异常
发现以前代码有些用法不对,如果是为了保存文件,那就需要提示要求权限;如果是为了分享文件,则需要改用Android7.0提出的FileProvider,同时在intent中设置Uri的临时权限 FLAG_GRANT_READ_URI_PERMISSION ,并且该权限在对方app退出后自动失效(intent的stack内有效)。
另外选拍照图片是一个特例,原先传入的是DCIM下的文件路径,现在要改成用FileProvider转换成Uri,否则相机会抛出异常:

android.os.FileUriExposedException: file:///storage/emulated/0/DCIM/Camera/IMG_20180713_171929.jpg exposed beyond app through ClipData.Item.getUri()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1972)
    at android.net.Uri.checkFileUriExposed(Uri.java:2371)
    at android.content.ClipData.prepareToLeaveProcess(ClipData.java:963)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:10231)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:10216)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1661)
    at android.app.Activity.startActivityForResult(Activity.java:4617)
    at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:48)
    at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:77)
    at android.support.v4.app.ActivityCompatJB.startActivityForResult(ActivityCompatJB.java:26)
    at android.support.v4.app.ActivityCompat.startActivityForResult(ActivityCompat.java:146)
    at android.support.v4.app.FragmentActivity.startActivityFromFragment(FragmentActivity.java:937)
    at android.support.v4.app.FragmentActivity$HostCallbacks.onStartActivityFromFragment(FragmentActivity.java:1046)
    at android.support.v4.app.Fragment.startActivityForResult(Fragment.java:956)
    at android.support.v4.app.Fragment.startActivityForResult(Fragment.java:945)

我们来看看FileUriExposedException是个什么鬼。。。

The exception that is thrown when an application exposes a file:// Uri to another app.

This exposure is discouraged since the receiving app may not have access to the shared path. For example, the receiving app may not have requested the Manifest.permission.READ_EXTERNAL_STORAGE runtime permission, or the platform may be sharing the Uri across user profile boundaries.

Instead, apps should use content:// Uris so the platform can extend temporary permission for the receiving app to access the resource.

This is only thrown for applications targeting Build.VERSION_CODES.N or higher. Applications targeting earlier SDK versions are allowed to share file:// Uri, but it’s strongly discouraged.

总而言之,就是Android不再允许在app中把file://Uri暴露给其他app,包括但不局限于通过Intent或ClipData 等方法。

public static Uri fixFileProviderUri(Context context, File file, Intent action) {
    // FileProvider修正文件uri
    Uri uri = FileProvider.getUriForFile(context,
           context.getString(R.string.file_provider_auth), file);
    action.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    return uri;
}

通过FileProvider提供的getUriForFile,将file://路径转成content://,注意!这里需要加上FLAG_GRANT_WRITE_URI_PERMISSION,同时APP应该请求Camera和Storage权限。。。。。。
拍照和选图片搞定了,结果裁剪图片又出了问题,原来APP提供出去的路径,需要给裁剪APP赋值可写的权限:

for (ResolveInfo res : list) {
    // API 24 需要赋予每一个裁剪APP访问文件的读写权限
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // 图片文件只读
        context.getContext().grantUriPermission(res.activityInfo.packageName, imageFile,
            Intent.FLAG_GRANT_READ_URI_PERMISSION);
        // 裁剪输出文件需要可写
        context.getContext().grantUriPermission(res.activityInfo.packageName, uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    }
}

这里grantUriPermission如果传入的包名是自身app,等同于上面的addFlags添加的Intent.FLAG_GRANT_READ_URI_PERMISSION。
我滴妈呀,FileProvider的坑真多,Intent分享出去的Uri即要自己可读,又要给别的APP赋予可写的权限。。。。。。
参考文档:http://gelitenight.github.io/android/2017/01/29/solve-FileUriExposedException-caused-by-file-uri-with-FileProvider.html
链接打不开可以点PDF文档快照:使用FileProvider解决file___ URI引起的FileUriExposedException

如果FileProvider的坑到这里就结束了,我会灰常开森。。。
下面介绍由content://引申的又一个坑,由于Intent之间传递禁止了file://头,所以APP内部传递文件句柄,也必须经FileProvider的手,那么在onActivityResult得到的Uri也是content://,大家可能会想,以前拿到这种类型已经有开源的方法嘛,通过context.getContentResolver().query查询column = “_data”得到真实的文件路径。。。可惜抛出了异常:

    Caused by: java.lang.IllegalArgumentException: column '_data' does not exist

查阅官方对FileProvider的详细解释,它给出了标准的流程如下(Access the requested file):

    /*
     * When the Activity of the app that hosts files sets a result and calls
     * finish(), this method is invoked. The returned Intent contains the
     * content URI of a selected file. The result code indicates if the
     * selection worked or not.
     */
    @Override
    public void onActivityResult(int requestCode, int resultCode,
            Intent returnIntent) {
        // If the selection didn't work
        if (resultCode != RESULT_OK) {
            // Exit without doing anything else
            return;
        } else {
            // Get the file's content URI from the incoming Intent
            Uri returnUri = returnIntent.getData();
            /*
             * Try to open the file for "read" access using the
             * returned URI. If the file isn't found, write to the
             * error log and return.
             */
            try {
                /*
                 * Get the content resolver instance for this context, and use it
                 * to get a ParcelFileDescriptor for the file.
                 */
                mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                Log.e("MainActivity", "File not found.");
                return;
            }
            // Get a regular file descriptor for the file
            FileDescriptor fd = mInputPFD.getFileDescriptor();
            ...
        }
    }

上面这段话的核心意思,就是通过getContentResolver().openFileDescriptor持有Intent传递过来的文件句柄,已经赋予了相应的读写权限。
So,到底怎么搞好呀?如果只是简单的拿文件,直接读数据倒简单;不过大部分代码应该是存储了文件路径,并且在APP关闭或者手机重启之后继续访问;
这个问题有点棘手,尽量在onActivityResult之后立即处理数据,否则拷贝文件带来性能开销、管理文件也需要额外处理。
唔,步子迈的太大会扯着蛋。。。考虑到APP内部跳转Intent携带的Uri路径是以本包名格式固定的,特别处理一下算了。
特别的举例:EXTERNAL_DIR,一般图省事,在res/xml/filepaths.xml中会定义外部存储的根路径

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- root-path/files-path/cache-path/external-path -->
    <external-path path="/" name="EXTERNAL_DIR" />
</paths>

那么转换Uri得到的路径是这样子的:

    content://包名.fileprovider/EXTERNAL_DIR/xxxx

So,我们通过Environment.getExternalStorageDirectory得到根路径,替代content://包名.fileprovider/EXTERNAL_DIR部分,就拿到了真实的文件路径。
哦,顺嘴提一句,上面这个函数虽然要求Storage权限,但是实际上无权限时,也能得到路径,只是不能访问读写文件而已。
注意,这样的替换只能针对APP内部Intent跳转,并且传递文件路径是在External Storage下的!!!

// APP内部Intent如果采用的EXTERNAL_DIR路径,可以直接转译成file://
public static final String EXTERNAL_DIR = "EXTERNAL_DIR";
@TargetApi(Build.VERSION_CODES.N)
public static Uri parseSelfUri(Uri uri) {
    final String path = uri.getPath();
    final String prefix = "/" + EXTERNAL_DIR;
    if (path.startsWith(prefix)) {
       final File sdcard = Environment.getExternalStorageDirectory();
       return Uri.fromFile(new File(path.replace(prefix, sdcard.getPath())));
    }
    return uri;
}
@TargetApi(Build.VERSION_CODES.N)
public static boolean isSelfPackageUri(Context context, Uri uri) {
    final String auth = uri.getAuthority();
    final String selfAuth = context.getString(R.string.file_provider_auth);
    if (auth != null && auth.equalsIgnoreCase(selfAuth)) {
        return true;
    }
    return false;
}

自测了一下APP内部传递Uri.fromFile(File)数据的情况,只有Intent#setData(Uri)与Intent#setClipData(ClipData)这两种传递Uri类型的方式,才需要限定采用content//auth.fileprovider/ 这种格式。
注意我用的是“两种传递Uri类型”,查看Intent的method,setData类型包含:setData、setDataAndNormalize、setDataAndType、setDataAndTypeAndNormalize。
God bless me!
审视代码中用到的Uri.fromFile以及Intent#setData、Intent#setClipData,只有选图片文件、裁剪图片才采用了上述类型。于是修改Intent返回的Uri用FileProvider修复,在应答里采用上面的parseSelfUri还原。
至此,FileProvider应该算是全部搞定,下面继续爬坑。(总结一下,FileProvider强制将APP内文件变成沙盒,对外不可见;为了分享对外可见,又额外的增加了Intent flag权限;基本上所有的核心问题都围绕着这个,蛋疼的还要结合Storage Permissions)

3、后台执行限制
包括后台Service、后台广播、后台位置限制三部分
① StartService
该方法只能是应用在前台时调用,否则抛出ANR异常。对此,提供了新的startForgroundService,启动前台服务。在Service启动后,需要调用startForground,提供一个前台Notification说明。这部分将在下边与通知的变更一块说明。
② 隐式广播
③ 后台位置限制

4、通知渠道
android.app.Notification.Builder增加了一个String channelId的构造参数,它是由NotificationManager#createNotificationChannel创建;
注意Notification.Builder去掉了setDefaults(Notification.DEFAULT_SOUND),由NotificationChannel的importance属性决定;同时增加了enableVibration、enableLights,实现震动和闪烁的提醒。
另外,上文startForground的第一个参数id,也是Notification的通知id,一般是默认自增字段,需要大于0;第二个参数是Notification对象,使用默认的channelId创建即可。

[m3u8]关于AES-128解码的尝试

上篇文章中m3u8文件采用了AES-128的加密方式,key是16位,还有iv参数,看样子是可以将ts解码出来,直接能播放。
传统的采用pycrypto模块
pip install pycrypto

示例代码如下:

    >>> from Crypto.Cipher import AES
    >>> from Crypto import Random
    >>>
    >>> key = b'Sixteen byte key'
    >>> iv = Random.new().read(AES.block_size)
    >>> cipher = AES.new(key, AES.MODE_CFB, iv)
    >>> msg = iv + cipher.encrypt(b'Attack at dawn')

这里的key是16位的byte,iv也是16位byte,上文中的iv是字符串“0xae98961dd802f860ae9b67dd75136a18”,需要转码

from binascii import unhexlify
iv = unhexlify('ae98961dd802f860ae9b67dd75136a18')

注意去掉字符串前面的0x前缀。

解析m3u8文件有开源的库,这里推荐:
https://github.com/globocom/m3u8.git

采用cipher.decrypt()得到的ts文件和源文件一样大,但是不能直接播放。。。so,肯定是哪里出问题了。。。。

突然发现openssl直接有提供aes-128带key和iv的解码cmd工具,用这个试试:

openssl enc -d -aes-128-cbc -iv {iv的十六进制字符串} -K {key的十六进制字符串} -in {输入ts文件} -out {输出ts文件}

通过OpenSSL这个直接输出的ts文件可以播放,泪流满面。。。。。。

在网上搜了一下这俩的实现:
Implement OpenSSL AES Encryption in Python
文章中提到:
The only non-standard (and most difficult) part is the derivation of the IV and the key from the password.

关于IV和key的不是标准部分…

OpenSSL puts and expects the salt in the first 8 bytes of the encrypted payload.

点开EVP_BytesToKey函数,有详细的说明。

难道说openssl需要读取加密的ts文件头8字节作为salt?