ART相关
1.1 Dalvik
我们知道 java是运行在java虚拟机JVM上,所以JAVA代码可移植性比较强。
Android是可以运行java代码的,(早期吃官司)所以其应该有一个Jvm虚拟机或者说实现了一个自己的Jvm虚拟机。Dalvik就是最初的Android平台上的Jvm实现,用以使得Android上能够运行java代码。Android4.4以前,使用的都是Dalvik虚拟机。
Dalvik采用的是jit编译,也就是即时编译,每次应用在运行时,它实时的将一部分 dex翻译成机器码。在程序的执行过程中,更多的代码被被编译并缓存。由于 JIT 只翻译一部分代码,它消耗的更少的内存,占用的更少的物理存储空间。
优点与缺点
优点 安装速度超快 存储空间小缺点 Multidex加载的时候会非常慢,因为在dex加载时会进行dexopt
JIT中需要解释器,解释器解释的字节码会带来CPU和时间的消耗 由于热点代码的Monitor一直在运行,也会带来电量的损耗
1.2 apk组成
APK 文件其实是 zip 格式,但后缀名被修改为 apk ,在 windows 上可以通过 winrar 等程序直接解压查看。
解压 apk 后,一般的可看到的目录结构如下:
文件或目录 | 作用 |
---|---|
META-INF/ | 信息描述,签名等用途。编译生成一个apk包时,会对所有要打包的文件做一个校验计算,并把计算结果放在META-INF目录下。而在Android手机上安装apk包时,应用管理器会按照同样的算法对包里的文件做校验,如果校验结果与META-INF下的内容不一致,系统就不会安装这个apk。这就保证了apk包里的文件不能被随意替换 |
res/ | 如果存在的话,存放的是 ndk 编出来的 so 库 |
libs/ | 存放的是 ndk 编出来的 so 库 |
AndroidManifest.xml | 程序全局配置文件 |
classes.dex | dalvik 字节码 |
resources.ars | 编译后的二进制资源文件,主要是对应的索引 |
assets/ | 保留工程中assets目录,其他工程下的、jar包中的assets也会合并到该assets目录下。 |
1.3 dex文件
dex 应该解释为: dalvik executable.
sdk有个工具dex,
把所有 javac 编译生成的 .class文件,打包成 classes.dex文件。
优化操作:
“打包”过程,存在优化操作,比如CassA和ClassB中都有一个字符串“Hello World!”,则ClassA和ClassB被打包到的同一个 classes.dex文件中,只存在一个字符串。这是打包过程中,其中的一个“优化”。
java bytecode转换为 dalvik byte code:
打包过程中,会把 .class文件中的所有 java bytecode 转换为 dalvik bytecode,因为 dalvik只能识别 dalvik bytecode,并执行它们.
简单来讲: Dalvik 执行 .dex文件,识别 .dex中的 dalvik bytecode并执行
1.4 vdex
android O 新增的格式包,dex代码 直接转化的 可执行二进制码 文件:
1.第一次开机就会生成在/system/app/packagename/oat/ 下;
2.在系统运行过程中,虚拟机将其 从 “/system/app” 下 copy 到 “/data/davilk-cache/” 下
1.5 odex
全名Optimized DEX,即优化过的DEX。
Apk在安装(installer)时,就会进行验证和优化,目的是为了校验代码合法性及优化代码执行速度,验证和优化后,会产生ODEX文件,运行Apk的时候,直接加载ODEX,避免重复验证和优化,加快了Apk的响应时间。
注意:优化过程会根据不同设备上Dalvik虚拟机的版本、Framework库的不同等因素而不同。在一台设备上被优化过的ODEX文件,拷贝到另一台设备上不一定能够运行。
优点:
1.减少了启动时间(省去了系统第一次启动应用时从apk文件中读取dex文件,并对dex文件做优化的过程。)和对RAM的占用(apk文件中的dex如果不删除,同一个应用就会存在两个dex文件:apk中和data/dalvik-cache目录下)。
2.防止第三方用户反编译系统的软件(odex文件是跟随系统环境变化的,改变环境会无法运行;而apk文件中又不包含dex文件,无法独立运行)。
在Android O 之后,odex 是从vdex 这个文件中 提取了部分模块生成的一个新的 可执行二进制码 文件 , odex 从vdex 中提取后,vdex 的大小就减少了。
1.第一次开机就会生成在/system/app/<packagename>/oat/ 下
2.在系统运行过程中,虚拟机将其 从 “/system/app” 下 copy 到 “/data/davilk-cache/” 下
3.odex + vdex = apk 的全部源码 (vdex 并不是独立于odex 的文件 odex + vdex 才代表一个apk )
1.6 art
odex 进行优化 生成的 可执行二进制码 文件,主要是apk 启动的常用函数相关地址的记录,方便寻址相关; 通常会在data/dalvik-cache/保存常用的jar包的相关地址记录。
1.第一次开机不会生成在/system/app/<packagename>/oat/ 下,以后也不会;(quicken模式的原因)
2.odex 文件在运行时,虚拟机会计算函数调用频率,进行函数地址的修改;
3.最后在/data/davilk-cache/ 由虚拟机生成;
4.生成art 文件后,/system/app 下的odex 和 vdex 会无效,即使你删除,apk也会正常运行
5.push 一个新的apk file 覆盖之前/system/app 下apk file ,会触发PKMS 扫描时下发force_dex flag ,强行生成新的vdex 文件 ,覆盖之前的vdex 文件,由于某种机制,这个新vdex 文件会copy到/data/dalvik-cache/下,于是art 文件也变化了。
1.7 oat
ART虚拟机使用的是oat文件,oat文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容。APK在安装的过程中,会通过dex2oat工具生成一个OAT文件。对于apk来说,oat文件实际上就是对odex文件的包装,即oat=odex,而对于一些framework中的一些jar包,会生成相应的oat尾缀的文件,如system@framework@boot-telephony-common.oat。
1.8 ART虚拟机
在5.0-7.0(Android N)之间,Android提出了ART虚拟机的概念,而运行的文件格式也从odex转换成了oat格式。
在APK安装的时候,PackageManagerService会调用dex2oat通过静态编译(AOT编译)的方式,来将所有的dex文件(包括Multidex)编译oat文件。
编译完后的oat其实是一个标准的ELF文件,只是相对于普通的ELF文件多加了oat data section以及oat exec section这两个段而已。
这两个段里面主要保存了两种信息:
Dex的文件信息以及类信息
Dex文件编译之后的机器码
在运行的时候,就直接运行oat的代码。而其中的Dex文件的内容也就是为了DexClassLoader在动态加载其他的Dex文件时,在链接的过程中可以找到对应的meta-data,正确的链接到引用的类文件与函数。
优点 运行时会超级快 在运行时省电,也节省各种资源
缺点
在系统更新的时候,所有app都需要进行dex2oat的操作,耗费的时间太长
在app安装的过程中,所耗费的时间也越来越长,因为apk包越来越大 由于oat文件中包含dex文件与编译后的Native
Code,导致占用空间也越来越大
由于上述的缺点,7.0之后的采用了 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,
解释器
JIT
OAT
将这三种方案进行混合编译,来从运行时的性能、存储、安装、加载时间进行平衡。
例如,Pixel 设备配置了以下编译流程:
最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译,会根据JIT运行时所收集的运行时函数调用的信息生成的Profile文件。
当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。
ART 的编译选项分为以下两个类别:
系统 ROM 配置:构建系统映像时,会对哪些代码进行 AOT 编译。
运行时配置:ART 如何在设备上编译和运行应用。
用于配置这两个类别的一个核心 ART 选项是“编译过滤器”。编译过滤器可控制 ART 如何编译 DEX 代码,是一个传递给 dex2oat 工具的选项。从 Android O 开始,有四个官方支持的过滤器:
verify:只运行 DEX 代码验证。
quicken:运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解译器性能。
speed:运行 DEX 代码验证,并对所有方法进行 AOT 编译。
speed-profile:运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。
系统 ROM 配置
有许多 ART 构建选项可用于配置系统 ROM。如何配置这些选项取决于 /system 的可用存储空间以及预安装应用的数量。编译到系统 ROM 中的 JAR/APK 可以分为以下四个类别:
启动类路径代码:默认使用 speed 编译过滤器进行编译。
系统服务器代码:默认使用 speed 编译过滤器进行编译。
产品专属的核心应用:默认使用 speed 编译过滤器进行编译。
所有其他应用:默认使用 quicken 编译过滤器进行编译。
Makefile 选项
WITH_DEXPREOPT
是否对系统映像上安装的 DEX 代码调用 dex2oat。默认处于启用状态。
DONT_DEXPREOPT_PREBUILTS(从 Android L 开始)
启用 DONT_DEXPREOPT_PREBUILTS 可防止对预构建的应用进行预先优化。这些都是在 Android.mk 中指定了 include $(BUILD_PREBUILT) 的应用,例如 Gmail。如果不对这些可能要通过 Google Play 更新的预构建应用进行预先优化,可以节省 /system 的空间,但是会增加首次启动时间。
PRODUCT_DEX_PREOPT_DEFAULT_COMPILER_FILTER(从 Android P 开始)
PRODUCT_DEX_PREOPT_DEFAULT_COMPILER_FILTER 可给经过预先优化的应用指定默认的编译过滤器。这些都是在 Android.mk 中指定了 include $(BUILD_PREBUILT) 的应用,例如 Gmail。如果未指定,则默认值为 quicken。
WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY(Android O MR1 中的新增选项)
如果启用 WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY,将只会对启动类路径和系统服务器 JAR 进行预先优化。
LOCAL_DEX_PREOPT
通过在模块定义中指定 LOCAL_DEX_PREOPT 选项,还可以针对个别应用启用或停用预先优化功能。这有助于避免对那些可能会立即收到 Google Play 更新的应用进行预先优化,因为更新之后,对系统映像中的代码所做的预先优化都成了无用功。此外,这还有助于在进行重大版本 OTA 升级时节省空间,因为用户的数据分区中可能已经有了更高版本的应用。
LOCAL_DEX_PREOPT 支持分别使用值“true”和“false”来启用和停用预先优化功能。此外,如果在预先优化过程中不应将 classes.dex 文件从 APK 或 JAR 文件中剥离出来,则可以指定“nostripping”。通常情况下,该文件会被剥离出来,因为在进行预先优化之后将不再需要该文件;但若要使第三方 APK 签名仍保持有效,则必须使用最后这个选项。
PRODUCT_DEX_PREOPT_BOOT_FLAGS
将选项传递给 dex2oat 以控制如何编译启动映像。该选项可用于指定自定义映像类列表、已编译类列表,以及编译过滤器。
PRODUCT_DEX_PREOPT_DEFAULT_FLAGS
将选项传递给 dex2oat 以控制如何编译除启动映像之外的所有内容。
PRODUCT_DEX_PREOPT_MODULE_CONFIGS
用于为特定模块和产品配置传递 dex2oat 选项。该选项在产品的 device.mk 文件中通过 $(call add-product-dex-preopt-module-config,<modules>,<option>) 设置,其中 <modules> 是一个 LOCAL_MODULE(表示 JAR 文件)和 LOCAL_PACKAGE(表示 APK 文件)名称的列表。
PRODUCT_DEXPREOPT_SPEED_APPS (New in Android O)
一个应用列表,其中的应用被确定为产品的核心应用,并且应使用 speed 编译过滤器进行编译。例如,常驻应用(如 SystemUI)只有在下次系统重新启动时才有机会使用配置文件引导型编译,因此可能最好是让产品始终对这些应用进行 AOT 编译。
PRODUCT_SYSTEM_SERVER_APPS (New in Android O)
系统服务器加载的应用的列表。这些应用将默认使用 speed 编译过滤器进行编译。
PRODUCT_ART_TARGET_INCLUDE_DEBUG_BUILD(Post Android O)
是否在设备上包含 ART 的调试版本。默认情况下,系统会针对 userdebug 和 eng build 启用该选项。可以通过将该选项明确设为“true”或“false”来替换此行为。
默认情况下,设备将使用非调试版本 (libart.so)。要进行切换,请将系统属性 persist.sys.dalvik.vm.lib.2 设置为 libartd.so。
WITH_DEXPREOPT_PIC (Removed in Android O)
从 Android 5.1.0 到 Android 6.0.1 的所有版本中,都可以通过指定 WITH_DEXPREOPT_PIC 启用位置无关代码 (PIC)。这样一来,就不必将来自映像的编译代码从 /system 迁移到 /data/dalvik-cache,因此可以节省数据分区中的空间。不过,因为该选项会停用根据位置相关代码进行的优化,所以会对运行时产生轻微的影响。通常情况下,需要节省 /data 空间的设备应启用 PIC 编译。
在 Android 7.0 中,PIC 编译默认处于启用状态。
WITH_DEXPREOPT_BOOT_IMG_ONLY(已在 Android O MR1 中移除)
此选项已被 WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY 取代,后者还可预先优化系统服务器 JAR。
预加载类列表
预加载类列表列出了 zygote 将在启动时初始化的类。利用该列表,每个应用无需单独运行这些类初始化程序,从而可以更快地启动并共享内存中的页面。预加载类列表文件默认位于 frameworks/base/preloaded-classes 中
最终保存在system/etc/preloaded-classes
cat了一下,基本都是我们android SDK中给app调用的一些类
软件包管理器选项
从 Android 7.0 开始,系统提供了一种通用方式来指定各个阶段的编译/验证级别。编译级别通过系统属性来配置,默认值如下:
pm.dexopt.install=quicken
安装应用时使用的编译过滤器。要加快安装速度,可以尝试使用 quicken 编译过滤器。
pm.dexopt.bg-dexopt=speed-profile
这是在设备闲置、充电以及充满电时使用的编译过滤器。要充分利用配置文件引导型编译并节省存储空间,可以尝试使用 speed-profile 编译过滤器。
pm.dexopt.boot=verify
无线下载更新后使用的编译过滤器。对于此选项,我们强烈建议使用 verify 编译过滤器,以防启动时间过长。
pm.dexopt.first-boot=quicken
在设备初次启动时使用的编译过滤器。此过滤器只会影响出厂后第一次启动的时间。我们建议使用 quicken 过滤器,以免用户在首次使用手机时需要花很长时间等待手机启动。请注意,如果 /system 中的所有应用都已使用 quicken 编译过滤器进行了编译,或者都已使用 speed 或 speed-profile 编译过滤器进行了编译,那么 pm.dexopt.first-boot 将不会产生任何影响。
如何查看我现在的某个应用是使用什么编译器呢?
dumpsys package 就可以查看到
Dexopt state:
[com.gitvkonka.video]
path: /data/app/com.gitvkonka.video-_9Gfd54CcNY0_LbT5xf8kg==/base.apk
arm: [status=speed-profile] [reason=install]
我们公司的平台上pm.dexopt.install=speed-profile,首次安装时系统中app的是没有profile文件,因此没有进行编译工作,相当于执行的是quicken一样的操作,所以为啥app用久了之后升级的时候安装时间会长一些。
为了证明优化效果
我这里拿银河奇异果应用来做了GPU呈现模式分析(FPS)测试,设置系统属性debug.hwui.profile为true,然后开始测试。通过命令adb shell dumpsys gfxinfo 包名 >/保存路径/fps.txt 。测试的思路是将银河奇异果应用采用pm.dexopt.install=quicken编译器和speed编译器安装应用,两个应用都是第一次安装并冷启动的条件下测试
speed:
微信截图_20200515164131.png
quicken:
微信截图_20200515164151.png
Draw: 表示在Java中创建显示列表部分中,OnDraw()方法占用的时间
Prepare: 准备时间
Process: 表示渲染引擎执行显示列表所花的时间,view越多,时间就越长
Execute: 表示把一帧数据发送到屏幕上排版显示实际花费的时间,其实是实际显示帧数据的后台缓存区与前台缓冲区交换后并将前台缓冲区的内容显示到屏幕上的时间
将上面的四个时间加起来就是绘制一帧所需要的时间,如果超过了16.67就表示掉帧了
从上面的数据来看,quicken模式下,是有掉帧的
1.9 profile文件
profile文件:/data/misc/profiles/cur/0/com.***.home/primary.prof
每个app的profile文件都在 /data/misc/profiles/ 目录下。profile文件用来记录运行比较频繁的代码,用来进行 profile-guide 编译,使得 dex2oat编译代码更精准。
profile的创建:
App安装的过程中,会调用到 installd的 create_app_data()函数,
如果当前支持profile编译,则会为app创建 profile文件。
profile信息的收集:
在App启动的时候,开启profile的收集线程:
->ActivityThread.main()
->...
->ActivityThread.performLaunchActivity()
->ActivityClientRecord.packageInfo.getClassLoader()
->LoadedApk.getClassLoader()
->setupJitProfileSupport()
VMRuntime.registerAppInfo(profileName)
Runtime::RegisterAppInfo(profileName)
jit_-> StartProfileSaver(profileName)
ProfileSaver::Start(profilName)//在这里会创建一个thread 用来收集 resolved class与method
可以使用profman工具查看profile文件 ,命令为profile --profile-file=primary.prof --dump-only
[图片上传失败...(image-c6b69a-1589532020941)]
2.0 odex优化的地方
1.首次开机或者升级
在SystemServer.java 中有mPackageManagerService.updatePackagesIfNeeded()
这里先列举流程,具体步骤有空再贴上
updatePackagesIfNeeded->performDexOptUpgrade->performDexOptTraced->performDexOptInternal->performDexOptInternalWithDependenciesLI->PackageDexOptimizer.performDexOpt->performDexOptLI->dexOptPath->Installer.dexopt->InstalldNativeService.dexopt->dexopt.dexopt
2.安装应用:
在PKMS.installPackageLI函数中有:
mPackageDexOptimizer.performDexOpt(pkg, pkg.usesLibraryFiles,
null /* instructionSets /, false / checkProfiles */,
getCompilerFilterForReason(REASON_INSTALL),
getOrCreateCompilerPackageStats(pkg),
mDexManager.isUsedByOtherApps(pkg.packageName));
3.IPackageManager.aidl提供了performDexOpt方法
在PKMS中有实现的地方,但是没找到调用的地方
4.IPackageManager.aidl提供了performDexOptMode方法
在PKMS中有实现的地方,在PackageManagerShellCommand中会被调用,应该是提供给shell命令调用
5.OTA升级后:
在SystemServer.java 中有OtaDexoptService.main(mSystemContext, mPackageManagerService);
public static OtaDexoptService main(Context context,
PackageManagerService packageManagerService) {
OtaDexoptService ota = new OtaDexoptService(context, packageManagerService);
ServiceManager.addService("otadexopt", ota);
// Now it's time to check whether we need to move any A/B artifacts.
ota.moveAbArtifacts(packageManagerService.mInstaller);
return ota;
}
private void moveAbArtifacts(Installer installer) {
if (mDexoptCommands != null) {
throw new IllegalStateException("Should not be ota-dexopting when trying to move.");
}
//如果不是升级上来的,就return掉
if (!mPackageManagerService.isUpgrade()) {
Slog.d(TAG, "No upgrade, skipping A/B artifacts check.");
return;
}
installer.moveAb(path, dexCodeInstructionSet, oatDir);
}
moveAbArtifacts函数的逻辑:
1.判断是否升级
2.判断扫描过的package是否有code,没有则跳过
3.判断package的code路径是否为空,为空则跳过
4.如果package的code在system或者vendor目录下,跳过
5.满足上述条件,调用Installer.java中的moveAb方法
最终是调用dexopt.cpp的move_ab方法
OtaDexoptService也提供给shell命令一些方法来调用
6.在系统空闲的时候:
是通过BackgroundDexOptService来实现的,BackgroundDexOptService继承了JobService
这里启动了两个任务
1.开机的时候执行odex优化 JOB_POST_BOOT_UPDATE
执行条件:开机一分钟内
PMS启动时添加Flag SCAN_NO_DEX, 当扫描执行目录时,跳过做Dex优化步骤,其次在SystemServer::startOtherServices时启动JobSchedule任务,延迟一分钟进行Dex任务;
1.1 扫描指定目录添加flag
public PackageManagerService(Context context, Installer installer, boolean factoryTest, boolean onlyCore) {
...
scanDirLI(customFrameworkDir, PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR,
scanFlags | SCAN_NO_DEX, 0); //添加flag
...
}
1.2 SCAN_NO_DEX
private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, int parseFlags,
int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {
...
if ((scanFlags & SCAN_NO_DEX) == 0) { // PackageMS启动时不做Dex优化
int result = mPackageDexOptimizer.performDexOpt(pkg, null /* instruction sets */,
forceDex, (scanFlags & SCAN_DEFER_DEX) != 0, false /* inclDependencies */);
if (result == PackageDexOptimizer.DEX_OPT_FAILED) {
throw new PackageManagerException(INSTALL_FAILED_DEXOPT, "scanPackageLI");
}
}
...
}
1.3 启动JobSchedule延迟Dex优化
private void startOtherServices() {
...
mSystemServiceManager.startService(JobSchedulerService.class);
traceBeginAndSlog("StartBackgroundDexOptService");
try {
BackgroundDexOptService.schedule(context);
} catch (Throwable e) {
reportWtf("starting BackgroundDexOptService", e);
}
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
...
mSystemServiceManager.startBootPhase(SystemService.PHASE_LOCK_SETTINGS_READY);
...
}
1.4 BackgroundDexOptService延迟执行Dex优化
public static void schedule(Context context) {
JobScheduler js = (JobScheduler)
context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
// Schedule a one-off job which scans installed packages and updates
// out-of-date oat files.
js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE,
sDexoptServiceName)
.setMinimumLatency(TimeUnit.MINUTES.toMillis(1)) //延迟一分钟
.setOverrideDeadline(TimeUnit.MINUTES.toMillis(1))
.build());
// Schedule a daily job which scans installed packages and compiles
// those with fresh profiling data.
js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(TimeUnit.DAYS.toMillis(1)) // 每天
.build());
}
@Override
public boolean onStartJob(JobParameters params) {
....
if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
return runPostBootUpdate(params, pm, pkgs);
} else {
return runIdleOptimization(params, pm, pkgs);
}
}
2.在系统休眠的时候执行优化 JOB_IDLE_OPTIMIZE
执行条件:设备处于空闲,插入充电器,且每隔一分钟或者一天就检查一次(根据debug开关控制) 按目前的情况来说,我们的电视是无法触发的,为什么?
2.1 JobScheduler机制
举例说明一个场景,当设备在空闲状态, 并且使用wifi时, 自动下载新的Apk。
那么我们可能会这样做:
对于延迟时间执行,通常考虑利用系统的闹钟管理器AlarmManager进行定时管理
对于是否联网、是否充电、是否空闲,一般要监听系统的相应广播,常见的系统广播说明如下:
网络状态变化需要监听系统广播android.net.conn.CONNECTIVITY_CHANGE;
设备是否充电需要监听系统广播Intent.ACTION_POWER_CONNECTED也就是android.intent.action.ACTION_POWER_CONNECTED;
设备是否空闲需要监听系统广播Intent.ACTION_SCREEN_OFF也就是android.intent.action.SCREEN_OFF;
Android从5.0开始,增加支持一种特殊的机制,即任务调度JobScheduler,该工具集成了常见的几种运行条件,开发者只需添加少数几行代码,即可完成原来要多种组件配合的工作,使代码变得更加优(牛)雅(x)
1.创建JobService
我们具体的业务逻辑还是要写在jobService中的, 所以自定义一个服务继承自JobService 并重写两个抽象方法
onStartJob:在任务开始执行时触发。返回false表示执行完毕,返回true表示需要开发者自己调用jobFinished方法通知系统已执行完成。
onStopJob,在任务停止执行时触发。
<service
android:name=".service.MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
2.Activity中配置JobInfo
JobInfo是从来描述任务的执行时间,条件,策略等一系列的行为,使用Builder模式来获取实例
setRequiredNetworkType:设置需要的网络条件,有三个取值:-
JobInfo.NETWORK_TYPE_NONE(无网络时执行,默认)、JobInfo.NETWORK_TYPE_ANY(有网络时执行)、JobInfo.NETWORK_TYPE_UNMETERED(网络无需付费时执行)
setPersisted:重启后是否还要继续执行,此时需要声明权限RECEIVE_BOOT_COMPLETED,否则会报错“java.lang.IllegalArgumentException: Error: requested job be persisted without holding RECEIVE_BOOT_COMPLETED permission.”而且RECEIVE_BOOT_COMPLETED需要在安装的时候就要声明,如果一开始没声明,而在升级时才声明,那么依然会报权限不足的错误。
setRequiresCharging:是否在充电时执行
setRequiresDeviceIdle:是否在空闲时执行
setPeriodic:设置时间间隔,单位毫秒。该方法不能和
setMinimumLatency、setOverrideDeadline这两个同时调用,否则会报错“java.lang.IllegalArgumentException: Can't call setMinimumLatency() on a periodic job”,或者报错“java.lang.IllegalArgumentException: Can't call setOverrideDeadline() on a periodic job”。
setMinimumLatency:设置至少延迟多久后执行,单位毫秒。
setOverrideDeadline:设置最多延迟多久后执行,单位毫秒。
setBackoffCriteria: 退避策略 , 可以设置等待时间以及重连策略
build:完成条件设置,返回构建好的JobInfo对象。
这里需要注意的一个点是JobScheduler所创建并执行的任务必须是带有条件限制的,不然是违背其初衷的,当你创建一个任务,并且不设置任何限制条件并且直接调用 scheduler.schedule(builder.build());去执行该任务是不可行的,会报以下的异常
java.lang.IllegalArgumentException: You're trying to build a job with no constraints, this is not allowed.
如何实现上面的限制的呢?
public JobSchedulerService(Context context) {
super(context);
...
// Create the controllers.
mControllers = new ArrayList<StateController>();
mControllers.add(new ConnectivityController(this));
mControllers.add(new TimeController(this));
mControllers.add(new IdleController(this));
mBatteryController = new BatteryController(this);
mControllers.add(mBatteryController);
mStorageController = new StorageController(this);
mControllers.add(mStorageController);
mControllers.add(new BackgroundJobsController(this));
mControllers.add(new ContentObserverController(this));
mDeviceIdleJobsController = new DeviceIdleJobsController(this);
mControllers.add(mDeviceIdleJobsController);
...
}
- DeviceIdleJobsController
eviceIdleJobsController用来控制Job对Doze的依赖条件,或者也可以说Doze对Job的限制,当设备进入Doze模式的IDLE状态时,将会限制除了Doze白名单外的所有应用的Job调度,当Doze退出IDLE状态进入维护状态后,将会对所有应用的Job解除限制。而DeviceIdleJobsController中则是通过广播的形式来感知Doze模式的状态变化,在其构造方法中可以看到广播的注册和监听
- IdleController
IdleController用来控制设备闲置状态对Job的约束,仅仅对设置过setRequiresDeviceIdle(true)的Job有效。
注意,这个闲置状态和Doze模式中的IDLE状态不是同一个概念,在IdleController中,会注册三类广播:亮灭屏广播、开始/结束屏保广播和无线充电广播,同时在配置文件config.xml中配置了一个阀值,规定当进入收到灭屏等广播后,到达这个阀值时,认定设备处于空闲状态。
3.TimeController
TimeController是用来控制截止时间和延迟时间对Job的约束,仅对设置了setOverrideDeadline()和setMinimumLatency()的Job有效。
-
BatteryController
BatteryController用来控制充电状态和低电量模式对Job的约束,仅仅对设置过setRequiresBatteryNotLow(true)或setRequiresCharging(true)的Job有效。 -
BackgroundJobsController后台限制
BackgroundJobsController用来控制Job后台的运行。由于AppStandby机制,当应用处于后台时,会进行一些功能的限制,以达到优化功耗的目的,BackgroundJobsController中正是对"当应用处于后台时限制它的Job"实现的地方。
和前几个控制器不同的是,BackgroundJobsController中监测某应用能否在后台运行Job,不是通过广播实现的,而是通过AppStateTracker和其内部接口AppStateTracker.Listener实现,而AppStateTracker是和AppStandby相关联的用来记录应用状态的类,当AppStandby中状态发生变化时,AppStateTracker.Listener的几个方法将被回调,从而更新各个应用的Job的后台约束:
- ConnectivityController
ConnectivityController用来控制网络对Job的约束,仅仅对设置过setRequiredNetwork()或setRequiredNetworkType()的Job有效。
ConnectivityController中通过接口回调的方式获取新的网络状态:
-
StorageController
StorageController用来控制存储空间对Job的约束,仅仅对设置过setRequiresStorageNotLow(true)的Job有效。其内部也是通过广播的形式获取设备是否处于低存储状态: -
ContentObserverController
ContentObserverController用来监测content URIS对Job的约束,仅仅对设置过addTriggerContentUri(Uri)的Job有效,当该URI发生变化后,将运行Job
鸣谢众大佬和官网的指导,受益匪浅:
https://www.cnblogs.com/danyuzhu11/p/11714220.html
https://blog.csdn.net/fightfightfight/article/details/86705847
https://www.jianshu.com/p/aa957774a965
https://blog.csdn.net/u011311586/article/details/83027820
https://www.jianshu.com/p/636eb2e12d54
https://blog.csdn.net/shuttlecheng/article/details/79018014
https://blog.csdn.net/hl09083253cy/article/details/78418809
官网:
https://source.android.google.cn/devices/tech/dalvik/configure?hl=zh-cn
网友评论