你可能不知道的Activity启动的诡异现象探索
这篇文章主要记录一下遇到的android启动过程中的一个有意思的探索过程,可能文章会比较长,相信我只要读下去一定会有所收获。这里说明一下,这篇文章肯定会涉及到activity的启动流程,很多文章都已经介绍了,这里不会带着大家看具体函数,因为太枯燥了,包括我本人也看不下去,力图简单明了。
一、问题的出现
最早这个问题是测试发现的一个怪异的现象。在登录失效的情况下,在我们的应用的个人页面进行手动刷新,会有很多接口在请求回来后,发登录失效的广播,而我们的广播处理比较简单,去启动一个设置为singleTop启动模式的LoginActivity。注意这时可能有多个广播出现。为了方便说明,这里假定只有两个,调试发现,调起LoginActivity后,点击back键,必须点两次才能回退成功,第一次貌似没反应,因为我们的应用是建立在插件框架的基础上的,第一次看到这个现象的时候,怀疑是插件框架导致singleTop失效,当把Activity的生命周期打印出来后,发现了一个诡异的现象:调用了两次startActivity,发现LoginActivity的onCreate方法只执行了一次,但是当我点击back键后,LoginActivity的onCreate又执行了一次。 什么,点击back键新建了一个Activity,你相信吗? 上面的问题可以简化成以下情景来表述。新建两个Activity:MainActivity和SecondActivity。代码非常简单,MainActivity只放置个Button,点击一次,启动两次Activity,SecondActivity就更简单了,只打印生命周期即可,代码如下: MainActivity.java:
1 | public void clickToStart(View view){ |
SecondActivity.java:
1 | public class SecondActivity extends Activity { |
当我单击一下按钮,你可以猜一下生命周期的回调顺序,这里就不卖关子了,直接打印出来的是:
1 | 05-18 22:57:38.870 25377-25377/com.example.ljj.test2 I/lan: MainActivity->onPause |
是不是有点吃惊,明明启动两次,却只执行了一次onCreate,按道理如果是singleTop生效的话,起码也得是一次onCreate和一次onNewIntent吧。此时点击back键,打印生命周期会发现已经启动的SecondActivity执行了onPause,然后又新建了一个SecondActivity。
1 | 05-18 22:57:49.924 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onPause: 29211210 |
为了方便研究,我们可以尝试着把singleTop启动模式去掉(不要怀疑手动设置Flag和manifest下写死的区别,singleTop模式下是等效的),执行同样的操作,你会发现standard启动模式下,现象也是一样的。 相信现象我已经阐述的很清楚了,现在总结一下,大概有两个问题: 第一,为什么标准启动模式下启动两次activity,只执行了一次onCreate方法,另一次是在onPause后才执行。 第二,为什么singleTop模式在这种情景下会失效。
二、针对问题的思考与初步探索
首先,我们把第二个问题先放一放,根源应该是在第一个问题上,先看一下针对打印出来的log的思考。SecondActivity确实是启动了两次,那到底先启动的是第一个,还是点击一次回退键后启动的是第一个?这里我们假设一下: 假设一:最先启动的是我们第一次执行startActivity启动的那个目标Activity,那么说明第二次执行的startActivity的目标Activity被暂存了,但是暂存在了哪里呢?好像AMS没有这样的功能 假设二:最先启动的是我们第二次执行startActivity启动的那个目标,可能因为启动太快,导致第一次启动的Activity被压到栈里,但不是栈顶,当SecondActivity执行到onPause时,它才有机会重见天日。 怎么验证呢,这时候不要忘了adb shell dumpsys的功能。当我们点击click按钮后,执行adb shell dumpsys activity activities >log.txt来查看系统中所有的activity信息。
1 | ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities) |
这里为什么要贴这么长的信息,因为这段信息非常重要,而且想说一下activity是如何管理的,或者说这段信息怎么看? 上图中显示了AMS中对Activity的管理结构。注意这些数据结构都是运行在系统进程中的。这里介绍下这些数据结构,让大家有个比较清晰的认识,不然在看Activity启动流程时很容易被这些栈搞晕。 ActivityRecord:Activity管理的最小单位,我们在应用进程创建一个activity,在AMS进程中就会生成一个ActivityRecord与之对应,类似于 packageName,launchedFromUid等都是ActivityRecord类的成员变量。 ActivityRecord源码链接(基于android5.0)
1 | * Hist #0: ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322} |
TaskRecord:是一个栈式管理结构,每一个TaskRecord都可能存在一个或多个ActivityRecord,栈顶的ActivityRecord表示当前可见的界面,这里需要注意的是 我们平时讨论的Activity任务栈实际上指的就是TaskRecord对象,而不是ActivityStack对象,包括我们如果同时启动多个应用,只是会在同一个ActivityStack中生成多个TaskRecord而已。简单理解就是TaskRecord就是我们经常说的任务栈。同理,userId,affinity等也是TaskRecord类的成员变量,可自行查看源码了解。
1 | * TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3} |
ActivityStack:也是一个栈式管理结构,每一个ActivityStack都可能存在一个或多个TaskRecord,栈顶的TaskRecord表示当前可见的任务;一般情况下,会存在两个ActivityStack,一个是应用的Stack,还有一个HomeStack。当我们启动应用或者从后台切换应用到前台时,应用Stack会被切换到栈顶,反之HomeStack会被切换到栈顶。对应于log信息为:
1 | Stack #1: |
ActivityDisplay: 是ActivityStackSupervisor的一个内部类,它对应于一个显示设备,不考虑多屏显示的情况下,就是指的手机屏幕,所以一般情况下,维护的ActivityDisplay数组的长度为1. ProcessRecord:每个进程会生成一个ProcessRecord对象,所有的ProcessRecord对象都保存在AMS的一个SparseArray类型的mPidsSelfLocked变量里。运行在不同TaskRecord中的ActivityRecord可能是属于同一个 ProcessRecord。ProcessRecord非常重要,假设我们创建了一个ActivityRecord,默认情况下,是没有和进程关联的,通过ProcessRecord的addPackage方法我们可以添加ActivityRecord到ProcessRecord中,还需要将ProcessRecord绑定到ActivityRecord上,这个ActivityRecord才是一个有“生命”,有“交互能力”的Activity。
通过上面的介绍,相信再看上面的信息会非常容易了。我们仔细观察dumpsys出来的信息,会发现两个地方值得注意。
1 | Running activities (most recent first): |
当前TaskRecord中乍一看只有两个ActivityRecord,但是不要被蒙蔽了,此时的sz=3,笔者最早看的时候时候就被蒙蔽了,所以要仔细看详细信息。 第二个异常点在Hist #1的activity的state是NITIALIZING。从字面上理解就是在初始化状态。
1 | Hist #1: ActivityRecord{3419ba07 u0 com.example.ljj.test2/.SecondActivity t1322} |
至此,我们大致能得出初步结论了,启动两次Activity,确实有两个Activity被放入了TaskRecord中了。栈顶的Activity能正常启动,被栈顶压住的Activity为INITIALIZING状态。那么问题又来了,INITIALIZING状态是个什么鬼?从源码角度如何解释这种生命周期的回调顺序?
三、源码维度探索
接下来会从源码的角度进行分析,里面会涉及到一些Activity启动流程相关的东西,不清楚的同学可以找其他文章学习下,直接看也没关系,因为不会涉及到很多代码细节。我们都知道activity的启动是和当前resume状态Activity的onPause事件相关,所以我们集中精力在onPause事件回调前的启动流程上,忽略后面的流程。 上面这张图简单的展示了当我们发起startActivity到前一个actiivty执行onPause的流程,讲解之前大致说一下Activity启动过程中应用进程和AMS进程通信的过程。
上面这种图清晰的展示了应用进程和AMS交互的方式。 大家可以简单的这样理解,两个进程通信就是通过两个顶层的Binder接口实现的: IApplicationThread:是系统进程请求应用进程的接口。Binder的服务端是运行在应用进程的,具体来讲就是ApplicationThread。AMS需要应用进程做的事情都是通过IApplicationThread接口中定义的方法来实现的。比如说schedulePauseActivity(告诉应用进程可以执行Activity的onPause了),scheduleLauchActivity(告诉应用进程现在可以执行启动Activity的操作了)等等。 IActivityManager:是应用进程请求系统进程的接口。Binder的服务端是运行在系统进程的,具体来讲就是ActivityManagerService。比如启动Activity的操作,实际上是向系统发出的申请,就是通过该接口的startActivity方法执行的。 了解到这里就足够啦,我们回过头来看我们的问题。我们先来看Activity启动图上面标红的部分,需要注意的是我们在应用进程发起的startActivity是带int返回值的,换句话说,就是返回值回来之前,主线程都是被挂起的,简单理解为是startActivity这个函数在获取返回值之前都是同步的。 Activity的启动源码确实非常复杂,上面标红的很多函数代码都有几百上千行。这里我可能有的地方直接给大家部分结论,感兴趣的同学自行去翻看源码。 我们先来看我们在onClick事件中第一次启动Activiy的过程。我们可以视作为这次启动是一次正常的启动。 在ActivityStackSuperVisor类可以看作是所有ActivityStack的管理者。从命名中也可以看出来,比如将哪个栈放到栈顶,管理当前前台的任务栈等都是ActivityStackSuperVisor完成的。在ActivityStackSuperVisor的startActivityLocked方法中会创建ActivityRecord对象,作为待启动Activity在系统进程的描述。那么是在哪个方法中将ActivityRecord放入栈中的呢?答案就是在ActivityStack的startActivityLocked方法,在该方法中会调用addActivityToTop方法将ActivityRecord放入到对应的TaskRecord的栈顶。 也就是说我们第一次启动Activity是肯定放入了栈中的。并且会顺利的执行到ActivityStack的startPausingLocked方法。
1 | final int startActivityUncheckedLocked(ActivityRecord r, ActivityRecord sourceRecord, |
可以看出来我们通过调用startActivity最终的返回值是由startActivityUncheckedLocked方法返回的。并且在返回前,会调用targetStack.startActivityLocked方法,最终会执行到startPausingLocked函数。
1 | final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming, |
注意我们是在分析第一次启动Activity的情形,mResumedActivity此时肯定是我们的MainActivity,通过上面代码执行后,prev,mPausingActivity,mLastPausedActivity指向了MainActivity,并且MainActivity的状态被更新为pausing状态,然后执行prev.app.thread.schedulePauseActivity方法,去通知应用进程MainActivity需要执行pause事件了。这里我们可以看到,pause事件是通过ActivityThread中的名为H的handler来处理的。
1 | public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges, |
看到这,我们在回过头来看我们的demo。你可以猜一下输出的log顺序。
1 | public void clickToStart(View view) { |
1 | 05-21 01:37:04.027 2084-2084/com.example.ljj.test2 I/lan: clickToStart:->once complete |
结果和我们上面分析的是一致的,因为onClick内的内容是在同一个Message中的。当我们第一次执行startActivity时,会抛出一个H.PAUSE_ACTIVITY的Message到主线程的消息队列里,但是这个消息的执行肯定是要等到onClick事件完全执行完才能处理的,这是消息队列的特点,必须等上一个消息执行完,才会轮到下一个消息的执行。 这里还差一点没有解释到位,就是在这种情境下,执行第二次startActivity时,在系统进程中走的流程和第一次启动一样吗?这里好像缺少了一些解释。上面我们分析到第一次startActivity,在startPausingLocked函数中,执行了Binder的schedulePauseActivity方法,该方法内发送了一个名为H.PAUSE_ACTIVITY的消息,按照正常逻辑,当应用内处理完H.PAUSE_ACTIVITY消息后,会向AMS发送通知,AMS接收到pause的通知后,会重新执行resumeTopActivityLocked函数,进而执行startSpecificActivityLocked函数来完成Activity真正的启动。但是在本文的情景中,这个H.PAUSE_ACTIVITY在被处理前,我们又执行了一次startActivity的操作,那么第二次启动会不会也执行到startPausingLocked时,再一次抛出去一个H.PAUSE_ACTIVITY呢,答案是不会的,分析如下:前面创建ActivityRecord和加入栈中操作都是一样的,同样会执行到startPausingLocked函数。
1 | final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming, |
注意,我们第一次启动时执行过一次startPausingLocked时,mResumedActivity已经被置为null,所以此时 执行ActivityRecord prev = mResumedActivity;相当于prev也为null了,这时候就会走if(prev==null)分支 了,返回了false。所以第二次启动是不会在抛出HH.PAUSE_ACTIVITY消息的。mStackSupervisor.resumeTopActivitiesLocked();会根据mResumedActivity和mPausingActivity状态进行各种判断,最后会在allPausedActivitiesComplete()方法判断中发现有activity执行pausing未完成,直接返回了false,受篇幅原因,具体的就不带着大家细看了,感兴趣的同学可以自行查阅。
1 | if (prev == null) { |
分析到这里,我们先来总结下在向下进行:到目前为止,我们得出的结论是: 假设我在同一个方法里面连续启动两次Activity,两个actiivty都会入栈,第一次启动的activity抛出了pause的Message,第二次没有抛出,而抛出的pause消息要等到第二次启动完成才能得到执行。当onPause执行完后,通知AMS要启动Activity了,此时AMS从栈顶取Activity,自然拿到的是第二个Activity,执行第二个Activity的onCreate事件。这样就完全解释清楚了上面现象的缘由。
四、 进一步验证
下面我们通过查看消息队列中的内容的方式来验证,查看源码发现,MessageQueue中提供了dump方法,可以获取到queue中的内容,接下来就简单了,反射拿到它,封装了个简单的MessageDump类。
1 | public class MessageDump { |
我们加入到代码中,分别在第一次启动前,第一次执行后,第二次执行后取dump消息队列:
1 | public void clickToStart(View view) { |
1 | 05-21 02:42:28.989 32626-32626/com.example.ljj.test2 I/lan: clickToStart:->before->first |
通过log我们清晰的看到确实在执行第一次启动后,消息队列中多了一条消息,并且在第二次启动完毕后,该消息依然存在,与我们之前的结论是相符的。
1 | { when=-1ms what=101 arg1=1 obj=android.os.BinderProxy@3e564eb2 target=android.app.ActivityThread$H } |
既然这样,那我第二次启动时通过Handler异步起动是不是就好了呢?
1 | public void clickToStart(View view) { |
结果我就不贴了,如果你理解了上文中讲的内容,应该可以猜到这种情形下,启动就正常了,因为我们第二次启动的消息是放在了pause消息之后了。
五、 singleTop失效的秘密
还有一个小问题,就是如果启动模式设置为singleTop的时候为什么会失效呢? 按道理来说,即使是第一次启动时被压入了栈中,没有正常启动,那intent信息总是在的啊,按道理不应该会影响启动模式才对。我们可以先看下IActivityManager的startActivity的返回值是什么?在执行startActivity时是会得到一个返回值的,这个返回值是反映Activity的启动状态的,和启动模式关系很大。
1 | public ActivityResult execStartActivity( |
我们通过动态代理来hook掉IActivityManager,可以拿到该返回值。这个返回值类型定义在了ActivityManager.java中,和启动模式相关的是下面这几个。
1 | public static final int START_SUCCESS = 0;//正常启动 |
通过hook拿到返回值会发现,连续两次启动时,返回值均为0,而加入handler异步起动后,得到的返回值是0,3。那就说明两次启动都是正常启动,但是第二次启动也是按照普通模式启动了,所以返回的是0。这下我们就可以有目的的去看源码了,只需要看到关于intent的处理部分即可。源码在ActivityStackSupervisor类的startActivityUncheckedLocked方法中。
1 | if (r.packageName != null) { |
我们重点来看判定条件,top是拿的栈顶的activity,此时是第一次启动放进去的SecondActiivty,说明一下,topRunningNonDelayedActivityLocked函数是拿取除了notTop指定的activity外,位于栈顶的activity。一般notTop为null。很显然第一层,第二层的if语句都是true。最里层的if语句呢?其实我一开始看的时候没有注意最里层的if,理所当然的认为不为null,导致这里找了半天都觉得说不通。
1 | ActivityStack topStack = getFocusedStack(); |
无奈之下,想从最早dump出来的信息里面找一下这个activity和正常启动的activity的差别。
原来在这,正常启动的activity都是绑定了ProcessRecord的,而INITIALIZING状态的Activity是没有进程相关信息的,也就是它不属于任何一个进程,还不具备和应用进程交互的能力。 真正绑定的地方在realStartActivityLocked方法中,也就是真正启动的过程中会进行绑定。现在就解释清楚了为什么singleTop在这种情境下失效了吧。
六. 实际应用场景的思考及解决办法
有人可能会说,实际场景中哪里有人会在一个函数中或者一个message中去启动两次activity?确实这种场景几乎没有,但是我们来看一下以下的情景。
1 | public void clickToStart(View view) { |
1 | private class MyReceiver extends BroadcastReceiver { |
我们用上述代码模拟了一下可能在实际代码中出现的场景,比如我们不同的网络接口请求,返回来后需要发送一个广播。我们应该都是post一个消息到主线程处理的,对应于消息1和消息2,如果恰好消息1和消息2被放入消息队列的时间比较接近,或者说是相邻。此时就会出现本文出现的现象。广播肯定也是进程间通信后,发送消息到主线程执行的,对应于消息3,消息4,所以当clickToStart执行完后,消息队列从头到尾:消息1->消息2,当消息1(发送广播)执行完后,变成:消息2->消息3;当消息2(发送广播)执行完后,变成:消息3->消息4。消息3和消息4挨着,两个消息的任务都是startActivity,那么很明显,这就是咱们的demo啊,肯定会有问题。不信可以自行尝试。怎么解决呢,有人说在onReceive里异步启动啊,其实是解决不了问题的,全部异步启动和不加异步是一样的,消息在队列里的顺序是不会改变的。 这种问题吧,首先要看下需不需要解决,说的直白点,也不能算是个bug,至于解决的方法,要和具体业务而定。具体解决的办法可以封装一个handler,该handler的任务保证唯一,或者指定what类型,在startActivity的函数中,remove同类型的信息,也就是上文中消息3->消息4,保证消息3->消息4的消息类型一致,然后在执行消息3后,remove掉消息4即可。
1 | public void clickToStart(View view) { |
这种方式也不太优雅,解决的办法肯定有很多,这里只是举个例子而已。知道了问题的缘由后,根据业务自行处理即可。
七. 总结
这篇文章主要是为了阐述一下activity连续启动的异常问题的跟踪过程。总结如下:
- 如果在同一个message连续启动同一个activity或者相邻message中分别启动同一个activity,就会出现生命周期诡异的问题,当然现在已经不诡异了。从探索方法和源码的角度给出了解释。
- 解释了为什么singleTop在这种模式下会失效的问题
- 给出了解决的思路,就是破坏消息顺序。
- 此外,阐述了Activity栈和启动activity流程相关的知识
env-centos-supervisor
简介
supervisor是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具。可以很方便的监听、启动、停止、重启一个或多个进程。用supervisor管理的进程,当一个进程意外被杀死,supervisor监听到进程死后,会自动将它重启,很方便的做到进程自动恢复的功能,不再需要自己写shell脚本来控制。
安装
配置好yum源后,可以直接安装
1 | yum install supervisor |
配置
安装好后在/etc/会生成一个supervisord.conf文件及一个supervisord.d文件目录
supervisord.conf是一些默认配置,可自行修改:
1 | [unix_http_server] |
注意:[include]
默认配置是制定*.ini
,因个人习惯命名为*.conf
文件,因此修改配置如下:
1 | [include] |
supervisord.d目录用来存放用户自定义的进程配置,参考:
1 | [program:es] |
注意: supervisor不能监控后台进程,command 不能为后台运行命令
服务段启动
1 | supervisord -c /etc/supervisord.conf |
常用命令介绍
supervisorctl 是 supervisord的命令行客户端工具
- supervisorctl status:查看所有进程的状态
- supervisorctl stop es:停止es
- supervisorctl start es:启动es
- supervisorctl restart es: 重启es
- supervisorctl update :配置文件修改后可以使用该命令加载新的配置
- supervisorctl reload: 重新启动配置中的所有程序
- … 把es 换成all 可以管理配置中的所有进程
直接输入:supervisorctl 进入supervisorctl 的shell交互界面,上面的命令不带supervisorctl 可直接使用
踩过的坑
unix:///var/run/supervisor/supervisor.sock no such file 问题描述:安装好supervisor没有开启服务直接使用supervisorctl报的错 解决办法:supervisord -c /etc/supervisord.conf
command中指定的进程已经起来,但supervisor还不断重启 问题描述:command中启动方式为后台启动,导致识别不到pid,然后不断重启,本人使用的是elasticsearch,command指定的是$path/bin/elasticsearch -d,踩到的坑 解决办法:supervisor无法检测后台启动进程的pid,而supervisor本身就是后台启动守护进程,因此不用担心这个
启动了多个supervisord服务,导致无法正常关闭服务 问题描述:在运行supervisord -c /etc/supervisord.conf 之前,我直接运行过supervisord -c /etc/supervisord.d/xx.conf,导致有些进程被多个superviord管理,无法正常关闭进程。 解决办法: 使用
ps -fe | grep supervisord
查看所有启动过的supervisord服务,kill相关的进程。
监控服务
监控nginx
在/etc/supervisord.d
下创建nginx.ini,填入:
1 | [program:nginx] |
由于supervisor不能监控后台程序,command = /usr/local/webserver/nginx/sbin/nginx
这个命令默认是后台启动,加上-g ‘daemon off;’这个参数可解决这问题,这个参数的意思是在前台运行。
监控redis
1 | command=redis-server /etc/redis.conf |
监控mongodb
1 | [program:mongod] |
监控wsgi
1 | [program:apiserver] |
未命名
1 | #user nobody; |
tips-android-version-addaptive
Android 7.0 (API 24) 适配
电池和内存
低电耗模式
此项新增的行为不会影响有关使您的应用适应Android 6.0(API级别23)中所推出的旧版本低电耗模式的建议和最佳做法,如对低电耗模式和应用待机模式进行针对性优化中所讨论
后台优化
移除了三项隐式广播,以帮助优化内存使用和电量消耗,详见后台优化
- CONNECTIVITY_ACTION:影响target 7.0的应用,替代方案JobScheduler
- ACTION_NEW_PICTURE:影响所有Android版本的应用,不允许收发
- ACTION_NEW_VIDEO:影响所有Android版本的应用,不允许收发
权限变更
系统权限变更
- 私有文件权限限制,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE会引起SecurityException
- file:// 受限制,会引起FileUriExposedException,推荐使用FileProvider
- DownloadManager受限制,7.0以前访问COLUMN_LOCAL_FILENAME可能会失败,7.0及以后会引起SecurityException,推荐使用ContentResolver.openFileDescriptor()
应用间共享文件
- target 7.0的App,Android框架禁止以file:// 形式分享文件,会引起FileUriExposedException,推荐使用content://,并授予URI临时访问权限,详情参阅共享文件
无障碍改进
屏幕缩放
屏幕尺寸放生变化时: |target <= 23| target > 23| |—|—| |前台| 配置变更通知 |配置变更通知| |后台 |Terminated |配置变更通知|
Setting中的视觉设置
Android 7.0在“Welcome”屏幕中加入了“Vision Settings”,用户可以在新设备上设置以下无障碍功能设置:Magnification gesture、Font size、Display size 和话语提示。此项变更让您可以更容易发现与不同屏幕设置有关的错误。要评估此功能的影响,您应在启用这些设置的状态下测试应用。您可以在 Settings > Accessibility 中找到这些设置。
NDK
Android 7.0开始,系统阻止应用动态链接非公开NDK库
检查
- target <= 23,运行在Android 7.0,且尝试访问私有库时,警告:
03-21 17:07:51.502 31234 31234 W linker : library “libandroid_runtime.so” (“/system/lib/libandroid_runtime.so”) needed or dlopened by “/data/app/com.popular-app.android-2/lib/arm/libapplib.so” is not accessible for the namespace “classloader-namespace” - the access is temporarily granted
- target > 23, 运行在Android 7.0,且尝试访问私有库时,可能崩溃:
java.lang.UnsatisfiedLinkError: dlopen failed: library “libcutils.so” (“/system/lib/libcutils.so”) needed or dlopened by “/system/lib/libnativeloader.so” is not accessible for the namespace “classloader-namespace” at java.lang.Runtime.loadLibrary0(Runtime.java:977) at java.lang.System.loadLibrary(System.java:1602)
- 静态检测工具readelf:
aarch64-linux-android-readelf -dW libMyLibrary.so
更新
如果您的应用使用私有平台库,您应更新它,以添加该应用自己的库副本或使用公开NDK API。
如果您的应用使用访问私有符号的第三方库,则联系库作者以更新库。
请确保将您的所有非NDK库与您的APK打包在一起。
使用标准JNI函数而非来自 libandroid_runtime.so 的 getJavaVM 和 getJNIEnv:
1
2
3AndroidRuntime::getJavaVM -> GetJavaVM from <jni.h>
AndroidRuntime::getJNIEnv -> JavaVM::GetEnv or
JavaVM::AttachCurrentThread from <jni.h>.使用 __system_property_get 而非来自 libcutils.so 的私有 property_get 符号。为此,请使用 __system_property_get 及以下 include 函数:
1
#include <sys/system_properties.h>
使用来自 libcrypto.so 的 SSL_ctrl 符号的本地版本。例如,您应在您的 .so 文件中静态链接 libcyrpto.a,或从 BoringSSL/OpenSSL 添加一个动态链接的 libcrypto.so 版本,并将其打包到您的APK中。
注解
Android 7.0修复了一个注解可见性被忽略的错误。这种问题会导致应用可在运行时访问原本不允许访问的注解。这些注解包括: VISIBILITY_BUILD:仅应编译时可见。 VISIBILITY_SYSTEM:运行时应可见,但仅限底层系统。 如果您的应用依赖这种行为,请为运行时必须可用的注解添加保留政策。您可通过使用@Retention(RetentionPolicy.RUNTIME)来执行此操作。
其他
- 显示尺寸变化时,target < 7.0的后台App进程会被终止。用户从最近使用记录中恢复应用时,会出现崩溃现象。
- Debug.startMethodTracing()方法不再需要WRITE_EXTERNAL_STORAGE权限。
- 许多平台API现在开始检查在 Binder 事务间发送的大负载,会引发TransactionTooLargeExceptions。常见例子:Activity.onSaveInstanceState() 上存储过多数据,导致 ActivityThread.StopInfo 引发RuntimeException。
- 如果应用向 View 发布 Runnable 任务,并且 View 未附加到窗口,系统会用 View 为 Runnable 任务排队;在 View 附加到窗口之前,不会执行 Runnable 任务
- 如果Android 7.0上一项有 DELETE_PACKAGES 权限的应用尝试删除一个软件包,但另一项应用已经安装了这个软件包,则系统需要用户进行确认。在这种情况下,应用在调用 PackageInstaller.uninstall() 时预计的返回状态应为 STATUS_PENDING_USER_ACTION。
- 名为 Crypto 的 JCA 提供程序已弃用,因为它仅有的 SHA1PRNG 算法为弱加密。应用无法再使用 SHA1PRNG(不安全地)派生密钥。
Android 8.0 (API 26) 适配
针对所有API级别的应用
后台执行限制
- 为了提供续航时间,对于收入后台(进入已缓存状态)、没有活动组件的App,系统会解除App所具有的所有唤醒锁
- 后台运行的应用对后台服务的访问受限
- 应用无法使用清单文件注册大部分隐式广播
默认只针对target 8.0的App,但是用户可以在Settings中手动启动这些限制,针对所有API级别的App
- 对特定API做了限制
- target 8.0的App在不允许创建后台服务的情况下调用startService()时,会引发IllegalStateException
- Context.startForegroundService()函数允许在后台运行的时候被调用,但是创建服务后要在5秒内调用该服务的startForeground()
详情参阅后台执行限制
后台位置限制
为节约电量和用户体验,限制后台应用接受位置更新的频率,受影响的API:
- Fused Location Provider (FLP)
- Geofencing
- GNSS Measurements
- Location Manager
- Wi-Fi Manager
详情参阅后台位置限制
应用快捷键
- com.android.launcher.action.INSTALL_SHORTCUT变为隐式广播,替代方案ShortcutManager
- ACTION_CREATE_SHORTCUT Intent功能增强
详情参见固定快捷方式和微件预览功能指南
语言区域和国际化
- 调用 Currency.getDisplayName(null) 会引发 NullPointerException,以与文档规定的行为保持一致。
- 时区名称的分析方法发生变化。之前,Android 设备使用在启动时取样的系统时钟值,缓存用于分析日期时间的时区名称。因此,如果在启动时或其他较为罕见的情况下系统时钟出错,可能对分析产生负面影响。 现在,一般情况下,在分析时区名称时分析逻辑将使用 ICU 和当前系统时钟值。此项变更可提供更加准确的结果,如果您的应用使用 SimpleDateFormat 等类,此结果可能与之前的 Android 版本不同。
- Android 8.0 将 ICU 的版本更新至版本 58。
Alert window
如果App使用SYSTEM_ALERT_WINDOW权限并且尝试下面某种窗口类型来显示Alert Window:
- TYPE_PHONE
- TYPE_PRIORITY_PHONE
- TYPE_SYSTEM_ALERT
- TYPE_SYSTEM_OVERLAY
- TYPE_SYSTEM_ERROR
- Target < 8.0,Alert始终显示在TYPE_APPLICATION_OVERLAY类型窗口的下方;
- Target >= 8.0,App会使用TYPE_APPLICATION_OVERLAY类型显示Alert
输入和导航
详情参阅支持键盘导航
网页表单自动填充
WebSettings:
- getSaveFormData() 函数现在返回 false。之前,此函数返回 true。
- 调用 setSaveFormData() 不再有任何效果。 WebViewDatabase
- 调用 clearFormData() 不再有任何效果。
- hasFormData() 函数现在返回 false。之前,当表单包含数据时,此函数返回 true。
无障碍功能
无障碍服务现在可以识别TextView对象内的所有ClickableSpan实例。
详情参阅无障碍功能
网络连接和HTTPS
Android 8.0 对网络连接和 HTTP(S) 连接行为做出了以下变更:
- 无正文的 OPTIONS 请求具有 Content-Length: 0 标头。之前,这些请求没有 Content-Length 标头。
- HttpURLConnection 在包含斜线的主机或颁发机构名称后面附加一条斜线,使包含空路径的网址规范化。例如,它将 http://example.com 转化为 http://example.com/。
- 通过 ProxySelector.setDefault() 设置的自定义代理选择器仅针对所请求的网址(架构、主机和端口)。因此,仅可根据这些值选择代理。传递至自定义代理选择器的网址不包含所请求的网址的路径、查询参数或片段。
- URI 不能包含空白标签。之前,平台支持一种权宜方法,即允许主机名称中包含空白标签,但这是对 URI 的非法使用。此权宜方法只是为了确保与旧版 libcore 兼容。开发者如果对 API 使用不当,将会看到一条 ADB 消息:“URI example..com 的主机名包含空白标签。此格式不正确,将不被未来的 Android 版本所接受。”Android 8.0 废除了此权宜方法;系统对格式错误的 URI 会返回 null。
- Android 8.0 在实现 HttpsURLConnection 时不会执行不安全的 TLS/SSL 协议版本回退。
- 对隧道 HTTP(S) 连接处理进行了如下变更:
- 在通过连接建立隧道 HTTP(S) 连接时,系统会在 Host 行中正确放置端口号 (:443) 并将此信息发送至中间服务器。之前,端口号仅出现在 CONNECT 行中。
- 系统不再将隧道连接请求中的 user-agent 和 proxy-authorization 标头发送至代理服务器。
- 在建立隧道时,系统不再将隧道 Http(s)URLConnection 中的 proxy-authorization 标头发送至代理。相反,由系统生成 proxy-authorization 标头,在代理响应初始请求发送 HTTP 407 后将其发送至此代理。同样地,系统不再将 user-agent 标头由隧道连接请求复制到建立隧道的代理请求。相反,库为此请求生成 user-agent 标头。
- 如果之前执行的 connect() 函数失败, send(java.net.DatagramPacket) 函数将会引发 SocketException。
- 如果存在内部错误,DatagramSocket.connect() 会引发 pendingSocketException。对于 Android 8.0 之前的版本,即使 send() 调用成功,后续的 recv() 调用也会引发 SocketException。为确保一致性,现在这两个调用均会引发 SocketException。
- 在回退到 TCP Echo 协议之前,InetAddress.isReachable() 会尝试执行 ICMP。
- 对于某些屏蔽端口 7 (TCP Echo) 的主机(例如 google.com),如果它们接受 ICMP Echo 协议,现在也许能够访问它们。
- 对于确实无法访问的主机,此项变更意味着调用需要两倍的时间才能返回结果。
蓝牙
Android 8.0 对 ScanRecord.getBytes() 函数检索的数据长度做出以下变更:
- getBytes() 函数对于所接收的字节数不作任何假定。因此,应用不应受所返回的任何最小或最大字节数的影响。相反,应用应当计算所返回数组的长度。
- 兼容蓝牙 5 的设备返回的数据长度可能会超出之前最大约 60 个字节的限制。
- 如果远程设备未提供扫描响应,则也可能返回少于 60 个字节的数据。
无缝连接
WLAN体验优化
安全性
Android 8.0 包含以下与安全性有关的变更:
- 此平台不再支持 SSLv3。
- 在与未正确实现 TLS 协议版本协商的服务器建立 HTTPS 连接时,HttpsURLConnection 不再尝试回退到之前的 TLS 协议版本并重试的权宜方法。
- Android 8.0 将使用安全计算 (SECCOMP) 过滤器来过滤所有应用。允许的系统调用列表仅限于通过 bionic 公开的系统调用。此外,还提供了其他几个后向兼容的系统调用,但我们不建议使用这些系统调用。
- 现在,您的应用的 WebView 对象将在多进程模式下运行。网页内容在独立的进程中处理,此进程与包含应用的进程相隔离,以提高安全性。
- 您无法再假定 APK 驻留在名称以 -1 或 -2 结尾的目录中。应用应使用 sourceDir 获取此目录,而不能直接使用目录格式。
- 如需了解与使用原生库有关的安全性增强的信息,请参阅原生库。
有关提升应用安全性的其他准则,请参阅面向 Android 开发者的安全性。
隐私性
Android 8.0 对平台做出了以下与隐私性有关的变更。
- 现在,平台改变了标识符的处理方式。
- 对于在 OTA 之前安装到某个版本 Android 8.0(API 级别 26)的应用,除非在 OTA 后卸载并重新安装,否则 ANDROID_ID 的值将保持不变。要在 OTA 后在卸载期间保留值,开发者可以使用密钥/值备份关联旧值和新值。
- 对于安装在运行 Android 8.0 的设备上的应用,ANDROID_ID 的值现在将根据应用签署密钥和用户确定作用域。应用签署密钥、用户和设备的每个组合都具有唯一的 ANDROID_ID 值。因此,在相同设备上运行但具有不同签署密钥的应用将不会再看到相同的 Android ID(即使对于同一用户来说,也是如此)。
- 只要签署密钥相同(并且应用未在 OTA 之前安装到某个版本的 O),ANDROID_ID 的值在软件包卸载或重新安装时就不会发生变化。
- 即使系统更新导致软件包签署密钥发生变化,ANDROID_ID 的值也不会变化。
- 要借助一个简单的标准系统实现应用获利,请使用广告 ID。广告 ID 是 Google Play 服务针对广告服务提供的唯一 ID,此 ID 可由用户重置。
- 查询 net.hostname 系统属性返回的结果为空。
记录未捕获的异常
如果某个应用安装的 Thread.UncaughtExceptionHandler 未移交给默认的 Thread.UncaughtExceptionHandler,则当出现未捕获的异常时,系统不会终止应用。从 Android 8.0 开始,在此情况下系统将记录异常堆栈跟踪情况;在之前的平台版本中,系统不会记录异常堆栈跟踪情况。 我们建议,自定义 Thread.UncaughtExceptionHandler 实现始终移交给默认处理程序处理。
联系人Provider关于使用情况统计变更
从Android 8.0开始,具有READ_CONTACTS权限的应用任然可以读取每个联系人的使用情况数据,但是获取的都是近似值而不是精确值(系统内部保留精确值),受影响的查询类型:
- TIMES_CONTACTED
- TIMES_USED
- LAST_TIME_CONTACTED
- LAST_TIME_USED
集合
现在,AbstractCollection.removeAll() 和 AbstractCollection.retainAll() 始终引发 NullPointerException;之前,当集合为空时不会引发 NullPointerException。
Android企业版
详情参阅企业中的 Android
参考
https://www.jianshu.com/p/d6c2cd72500d
Android 9.0 (API 28) 适配
针对所有API级别的应用
电源管理
帮助确保系统资源被提供给最需要它们的应用,详情参阅电源管理
隐私权
- 后台对传感器的访问受限 App后台运行时,以下行为受限
- 访问麦克风和摄像头
- 使用连续报告模式的传感器(加速度计、陀螺仪等)不会接收事件
- 使用变化或一次性报告模式的传感器不会接收事件
- 限制访问通话记录 引入CALL_LOG权限组,把READ_CALL_LOG、WRIT_CALL_LOG、PROCESS_OUTGOING_CALLS移入该组(以前在PHONE权限组)。
- 限制访问电话号码 要从手机状态中读取电话号码,需要:
- 要通过PHONE_STATE Intent读取,需要READ_CALL_LOG和READ_PHONE_STATE权限
- 要通过 onCallStateChanged() 读取,只需要READ_CALL_LOG权限
- 限制访问WiFi位置和连接信息
- WiFi扫描限制更严格,详情参阅Wi-Fi 扫描限制
- getConnectionInfo() 函数返回的 WifiInfo 对象受限,只有当App具有以下权限时,才能获得SSID和BSSID:
- ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION
- ACCESS_WIFI_STATE
- 还需要在设备上启用位置服务(Settings > Location)
- WiFi服务函数移除多余信息
在 Android 9 中,下列事件和广播不接收用户位置或个人可识别数据方面的信息: WifiManager 中的 getScanResults() 和 getConnectionInfo() 函数。 WifiP2pManager 中的 [discoverServices()](https://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html?hl=zh-cn#discoverServices(android.net.wifi.p2p.WifiP2pManager.Channel, android.net.wifi.p2p.WifiP2pManager.ActionListener)) 和 [addServiceRequest()](https://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html?hl=zh-cn#addServiceRequest(android.net.wifi.p2p.WifiP2pManager.Channel, android.net.wifi.p2p.nsd.WifiP2pServiceRequest, android.net.wifi.p2p.WifiP2pManager.ActionListener)) 函数。 NETWORK_STATE_CHANGED_ACTION 广播。 Wi-Fi 的 NETWORK_STATE_CHANGED_ACTION系统广播不再包含 SSID(之前为 EXTRA_SSID)、BSSID(之前为 EXTRA_BSSID)或连接信息(之前为 EXTRA_NETWORK_INFO)。 如果应用需要此信息,请改为调用getConnectionInfo()。
- 电话信息依赖设备定位设置 如果停用设备定位,则以下函数不提供结果:
- getAllCellInfo()
- [listen()](https://developer.android.com/reference/android/telephony/TelephonyManager.html?hl=zh-cn#listen(android.telephony.PhoneStateListener, int))
- getCellLocation()
- getNeighboringCellInfo()
non-sdk api的限制
为帮助确保应用稳定性和兼容性,此平台对某些非 SDK 函数和字段的使用进行了限制;无论您是直接访问这些函数和字段,还是通过反射或 JNI 访问,这些限制均适用。 在 Android 9 中,您的应用可以继续访问这些受限的接口;该平台通过 toast 和日志条目提醒您注意这些接口。 如果您的应用显示这样的 toast,则必须寻求受限接口之外的其他实现策略。 详情参阅对非 SDK 接口的限制 |列表|说明| |—|—| |黑名单|无论您应用的目标 API 级别是什么,您都无法使用此列表中的非 SDK 接口。如果您的应用尝试访问其中任何一个接口,系统就会抛出错误。| |灰名单|只要在您应用的目标 API 级别不限制此列表中的非 SDK 接口,您就可以使用它们。从 Android 9(API 级别 28)开始,我们在每个 API 级别分别会限制某些非 SDK 接口。如果您应用的目标 API 级别较低,您可以访问灰名单中的受限 API,但如果您的应用尝试访问在您的目标 API 级别受限的非 SDK 接口,系统就会假定此 API 已列入黑名单。注意:在 Android 9(API 级别 28)中,非受限灰名单中的非 SDK 接口称为浅灰名单,而受限灰名单中的非 SDK 接口称为深灰名单。| |白名单|此列表中的接口已在 Android 框架软件包索引中正式记录,它们是受支持的接口,您可以自由使用。|
- 使用veridex工具测试 veridex 工具会扫描 APK 的整个代码库(包括所有第三方库),并报告发现的所有使用非 SDK 接口的行为。但是也有局限性:
- 无法检测到通过 JNI 实现的调用。
- 只能检测到一部分通过反射实现的调用。
- 对非活动代码路径的分析仅限于 API 级别的检查。
- 运行StrictMode API测试 使用 detectNonSdkApiUsage 方法来启用此 API。启用 StrictMode API 后,您可以使用 [penaltyListener](https://developer.android.com/reference/android/os/StrictMode.VmPolicy.BuilderpenaltyListener(java.util.concurrent.Executor, android.os.StrictMode.OnVmViolationListener)?hl=zh-cn) 来接收每次使用非 SDK 接口的行为所对应的回调,并且您可以在其中实现自定义处理。回调中提供的 Violation 对象派生自 Throwable,并且封闭式堆栈轨迹会提供相应使用行为的上下文。
- 使用可调试的应用测试 通过在搭载 Android 9(API 级别 28)或更高版本的设备或模拟器上构建和运行可调试应用来测试该应用是否使用非 SDK 接口。请确保您使用的设备或模拟器与您应用的目标 API 级别相匹配。 在您的应用上运行测试时,如果该应用访问了某些非 SDK 接口,系统就会输出一条日志消息。您可以检查应用的日志消息,查找以下详细信息:
- 声明的类、名称和类型(采用 Android 运行时所使用的格式)。
- 访问方式:链接、反射或 JNI
- 所访问的非 SDK 接口属于哪个列表。
您可以使用 adb logcat 来查看这些日志消息,这些消息显示在所运行应用的 PID 下。举例而言,日志中可能包含如下条目:
Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
安全行为变更
设备安全性变更
传输层安全协议(TLS)实现变更
1 | ###### 更严格的SECCOMP过滤器 |
加密变更
参数和算法的 Conscrypt 实现 其他变更
不再支持Android安全加密文件
详情参阅安全行为变更
ICU更新
升级ICU库到60。受影响内容:
- 更好的区分GMT和UTC
- java.text.SimpleDateFormat的使用
- java.text.DateFormatSymbols.getZoneStrings()的使用
- 亚洲/河内不再是可识别的时区
- 使用NumberFormat.parseCurrency代替android.icu.text.NumberFormat.getInstance(ULocale, PLURALCURRENCYSTYLE).parse(String)去解析币种文本
详情参阅ICU更新
Android Test变更
详情参阅Android Test变更
Java UTF解码器
UTF-8 是 Android 中的默认字符集。 UTF-8 字节序列可由 String(byte[] bytes) 之类的 String 构造函数解码。 Android 9 中的 UTF-8 解码器遵循比以前版本中更严格的 Unicode 标准: 这些变更包括:
- 非最短形式的 UTF-8(例如 <C0, AF>)被视为格式不正确。
- 替代形式的 UTF-8(例如 U+D800..U+DFFF)被视为格式不正确。
- 最大的子部分被单个 U+FFFD 取代。 例如,在字节序列“41 C0 AF 41 F4 80 80 41”中,最大子部分为“C0”、“AF”和“F4 80 80”。其中“F4 80 80”可以是“F4 80 80 80”的初始子序列,但“C0”不能是任何形式正确的代码单位序列的初始子序列。 因此,输出应为“A\ufffd\ufffdA\ufffdA”。
- 要在 Android 9 或更高版本中解码修改后的 UTF-8/CESU-8 序列,请使用 DataInputStream.readUTF() 函数或 NewStringUTF() JNI 函数。
使用证书的主机名校验
RFC 2818中介绍了两种对照证书匹配域名的方法—使用 subjectAltName (SAN) 扩展程序中的可用名称,或者在没有 SAN 扩展程序的情况下,回退到 commonName (CN)。 然而,在 RFC 2818 中,回退到 CN 已被弃用。因此,Android 不再回退到使用 CN。 要验证主机名,服务器必须出示具有匹配 SAN 的证书。 不包含与主机名匹配的 SAN 的证书不再被信任。
网络地址查询可能导致网络违规
不运行在UI Thread中做网络请求,任何时候!
套接字标记
报告的套接字中可用字节数
在调用 shutdownInput() 函数后,available() 函数会在调用时返回 0。
更详尽的VPN网络功能报告
在 Android 8.1(API 级别 28)及更低版本中,NetworkCapabilities 类仅报告 VPN 的有限信息,例如 TRANSPORT_VPN,但会省略 NET_CAPABILITY_NOT_VPN。 信息有限导致难以确定使用 VPN 是否会导致对应用的用户收费。 例如,检查 NET_CAPABILITY_NOT_METERED 并不能确定底层网络是否按流量计费。 从 Android 9 及更高版本开始,当 VPN 调用 setUnderlyingNetworks() 函数时,Android 系统将会合并任何底层网络的传输和能力并返回 VPN 网络的有效网络能力作为结果。 在 Android 9 及更高版本中,已经检查NET_CAPABILITY_NOT_METERED 的应用将收到关于 VPN 网络能力和底层网络的信息。
应用不再能访问xt_qtaguid文件夹中的文件
从 Android 9 开始,不再允许应用直接读取 /proc/net/xt_qtaguid 文件夹中的文件。 这样做是为了确保与某些根本不提供这些文件的设备保持一致。 依赖这些文件的公开 API TrafficStats 和 NetworkStatsManager 继续按照预期方式运行。 然而,不受支持的 cutils函数(例如 qtaguid_tagSocket())在不同设备上可能不会按照预期方式运行 — 甚至根本不运行
现在强制执行 FLAG_ACTIVITY_NEW_TASK 要求
在 Android 9 中,您不能从非 Activity 环境中启动 Activity,除非您传递 Intent 标志 FLAG_ACTIVITY_NEW_TASK。 如果您尝试在不传递此标志的情况下启动 Activity,则该 Activity 不会启动,系统会在日志中输出一则消息。
屏幕旋转变更
从 Android 9 开始,对纵向旋转模式做出了重大变更。 在 Android 8.0(API 级别 26)中,用户可以使用 Quicksettings 图块或 Display 设置在自动屏幕旋转和纵向旋转模式之间切换。 纵向模式已重命名为旋转锁定,它会在自动屏幕旋转关闭时启用。 自动屏幕旋转模式没有任何变更。 当设备处于旋转锁定模式时,用户可将其屏幕锁定到顶层可见 Activity 所支持的任何旋转。 Activity 不应假定它将始终以纵向呈现。 如果顶层 Activity 可在自动屏幕旋转模式下以多种旋转呈现,则应在旋转锁定模式下提供相同的选项,根据 Activity 的 screenOrientation 设置,允许存在一些例外情况(见下表)。 请求特定屏幕方向(例如,screenOrientation=landscape)的 Activity 会忽略用户锁定首选项,并且行为与 Android 8.0 中的行为相同。 可在 Android Manifest 中,或以编程方式通过 setRequestedOrientation() 在 Activity 级别设置屏幕方向首选项。 旋转锁定模式通过设置 WindowManager 在处理 Activity 旋转时使用的用户旋转首选项来发挥作用。 用户旋转首选项可能在下列情况下发生变更。 请注意,恢复设备的自然旋转存在偏差,对于外形与手机类似的设备通常设置为纵向:
当用户接受旋转建议时,旋转首选项变为建议方向。 当用户切换到强制纵向应用(包括锁定屏幕或启动器)时,旋转首选项变为纵向。
Apache HTTP 客户端弃用影响采用非标准 ClassLoader 的应用
在 Android 6.0 中,我们取消了对 Apache HTTP 客户端的支持。 此变更对大多数不以 Android 9 或更高版本为目标的应用没有任何影响。 不过,此变更会影响使用非标准 ClassLoader结构的某些应用,即使这些应用不以 Android 9 或更高版本为目标平台。 如果应用使用显式委托到系统 ClassLoader 的非标准 ClassLoader,则应用会受到影响。 在 org.apache.http.*中查找类时,这些应用需要委托给应用 ClassLoader。 如果它们委托给系统 ClassLoader,则应用在 Android 9 或更高版本上将失败并显示 NoClassDefFoundError,因为系统 ClassLoader 不再识别这些类。 为防止将来出现类似问题,一般情况下,应用应通过应用 ClassLoader 加载类,而不是直接访问系统 ClassLoader。
枚举相机
在 Android 9 设备上运行的应用可以通过调用 getCameraIdList() 发现每个可用的摄像头。 应用不应假定设备只有一个后置摄像头或只有一个前置摄像头。
针对 Target >= 9.0 的应用
前台服务
针对 Android 9 或更高版本并使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限。 这是普通权限,因此,系统会自动为请求权限的应用授予此权限。未请求权限就创建服务会引起SecurityException
隐私权变更
构建序列号弃用 在 Android 9 中,Build.SERIAL 始终设置为 “UNKNOWN” 以保护用户的隐私。如果您的应用需要访问设备的硬件序列号,您应改为请求 READ_PHONE_STATE 权限,然后调用 getSerial()。
DNS 隐私
以 Android 9 为目标平台的应用应采用私有 DNS API。 具体而言,当系统解析程序正在执行 DNS-over-TLS 时,应用应确保任何内置 DNS 客户端均使用加密的 DNS 查找与系统相同的主机名,或停用它而改用系统解析程序。
框架安全性变更
默认情况下启用网络传输层安全协议 (TLS)
如果您的应用以 Android 9 或更高版本为目标平台,则默认情况下 isCleartextTrafficPermitted() 函数返回 false。 如果您的应用需要为特定域名启用明文,您必须在应用的网络安全性配置中针对这些域名将 cleartextTrafficPermitted 显式设置为 true 按进程分设基于网络的数据目录
为改善 Android 9 中的应用稳定性和数据完整性,应用无法再让多个进程共用同一 WebView 数据目录。 此类数据目录一般存储 Cookie、HTTP 缓存以及其他与网络浏览有关的持久性和临时性存储。 在大多数情况下,您的应用只应在一个进程中使用 android.webkit 软件包中的类,例如 WebView 和 CookieManager。 例如,您应该将所有使用 WebView 的 Activity 对象移入同一进程。 您可以通过在应用的其他进程中调用 disableWebView(),更严格地执行“仅限一个进程”规则。 该调用可防止 WebView 在这些其他进程中被错误地初始化,即使是从依赖内容库进行的调用也能防止。 如果您的应用必须在多个进程中使用 WebView 的实例,则必须先利用 WebView.setDataDirectorySuffix() 函数为每个进程指定唯一的数据目录后缀,然后再在该进程中使用 WebView 的给定实例。 该函数会将每个进程的网络数据放入其在应用数据目录内自己的目录中。 注:即使您使用 setDataDirectorySuffix(),系统也不会跨应用的进程界限共享 Cookie 以及其他网络数据。 如果应用中的多个进程需要访问同一网络数据,您需要自行在这些进程之间复制数据。 例如,您可以调用 getCookie() 和 [setCookie()](https://developer.android.com/reference/android/webkit/CookieManager.html?hl=zh-cn#setCookie(java.lang.String, java.lang.String, android.webkit.ValueCallback)),在不同进程之间手动传输 Cookie 数据。
以应用为单位的 SELinux 域名
以 Android 9 或更高版本为目标平台的应用无法利用可全球访问的 Unix 权限与其他应用共享数据。 此变更可改善 Android 应用沙盒的完整性, 具体地讲,就是要求应用的私有数据只能由该应用访问。要与其他应用共享文件,请使用 content provider。
连接变更
连接数据计数和多路径 在以 Android 9 或更高版本为目标平台的应用中,系统计算并非当前默认网络的网络流量,例如,当设备连接 WLAN 时的蜂窝流量,并在 NetworkStatsManager 类中提供函数以查询该流量。 具体而言,getMultipathPreference() 现在将返回一个基于上述网络流量的值。 从 Android 9 开始,此函数针对蜂窝数据返回 true,但当超过一天内累积的特定流量时,它将开始返回 false。 在 Android 9 上运行的应用必须调用此函数并采用此提示。 ConnectivityManager.NetworkCallback 类现在将有关 VPN 的信息发送到应用。 此变更让应用侦听连接事件变得更容易,而无需混用同步和异步调用,也无需使用有限的 API。 此外,它还意味着将设备同时连接至多个 WLAN 网络或多个蜂窝网络时,信息传输可按预期工作。
Apache HTTP 客户端弃用
在 Android 6.0 中,我们取消了对 Apache HTTP 客户端的支持。 从 Android 9 开始,默认情况下该内容库已从 bootclasspath 中移除且不可用于应用。 要继续使用 Apache HTTP 客户端,以 Android 9 及更高版本为目标的应用可以向其 AndroidManifest.xml 添加以下内容:
1 | <uses-library android:name="org.apache.http.legacy" android:required="false"/> |
注:拥有最低 SDK 版本 23 或更低版本的应用需要 android:required=”false” 属性,因为在 API 级别低于 24 的设备上,org.apache.http.legacy 库不可用。 (在这些设备上,Apache HTTP 类在 bootclasspath 中提供。) 作为使用运行时 Apache 库的替代,应用可以在其 APK 中绑定自己的 org.apache.http 库版本。 如果进行此操作,您必须将该库重新打包(使用一个类似 Jar Jar 的实用程序)以避免运行时中提供的类存在类兼容性问题。
界面变更
View Focus
0 面积的视图(即宽度或高度为 0)再也不能被聚焦。Activity 不再隐式分配触摸模式下的初始焦点,需要的话要显式请求初始焦点。
CSS RGBA 十六进制值处理
以 Android 9 或更高版本为目标的应用必须支持草案版 CSS 颜色模块级别 4 的行为,用于处理 4 和 8 个十六进制数字 CSS 颜色。 Chrome 自版本 52 以来便一直支持 CSS 颜色模块级别 4,但 WebView 目前停用此功能,因为现有 Android 应用被发现包含 Android ordering (ARGB) 中的 32 位十六进制颜色,这会导致渲染错误。 例如,对于以 API 级别 27 或更低版本为目标平台的应用,颜色 #80ff8080 目前在 WebView 中被渲染为不透明浅红色 (#ff8080)。 先导部分(Android 会将其解读为 Alpha 部分)目前被忽略。 如果某个应用以 API 级别 28 或更高版本为目标,则 #80ff8080 将被解读为 50% 透明浅绿 (#80ff80)。 文档滚动标签 Android 9 可正确处理文档的根标签是滚动标签的案例。 在之前的版本中,滚动位置在 body 标签上设置,根标签的滚动值为零。 Android 9 支持符合标准的行为,在这种行为中,滚动标签是根标签。 此外,直接访问 document.body.scrollTop、document.body.scrollLeft、document.documentElement.scrollTop 或 document.documentElement.scrollLeft 会因目标 SDK 的不同而具有不同的行为。 要访问视口滚动值,请使用 document.scrollingElement(若有)。 来自已暂停应用的通知 在 Android 9 之前,暂停的应用发出的通知会被取消。 从 Android 9 开始,暂停的应用发出的通知将被隐藏,直至应用继续运行。
Android电源管理基础知识整理
DozeMode模式
由于Android的开放特性,加上国内app开发者的觉悟普遍不高的情况下,越来越多的app开始利用安卓的系统特性甚至可以称为漏洞,故意让app退出后仍然占用大量的硬件资源。 越来越多的应用会在后台运行时“假死”,即不进入真正的Sleep,而是不断在后台轮询搜集用户行为或者保持某些长链接来保障数据的实时性。而Android系统自身并未出台对应的策略来约束或者限制这类应用行为,当这类应用越来越多,就会导致用户的Android设备电量消耗越来越高、手机越来越烫、流量偷跑、话费超标等情况。
google也已经意识到这个问题,并且从Android M(即6.0)开始引入 Doze Mode,中文翻译为“打旽模式”,但专业术语的翻译为“低电耗模式”和“应用待机模式”,android系统的这两个模式都是采用 Doze Mode 来实现的。
doze 这个单词的中文翻译为“打瞌睡”,“打瞌睡”的意思就是稍微休息一下,例如我们长时间工作时,可能会觉得疲倦,这时我们会趴在桌子上眯几分钟,但是我们的思想状态是可以随时进入工作状态的,比如领导随时可能给你安排一项紧急的工作,这时你可以立即进入工作状态。 那么对于 安卓系统而言,系统在锁屏后也会选择一个合适的时机休息片刻,一旦用户再次解锁屏,系统又能立即进入工作状态。
从 Android 6.0(API 级别 23)开始,Android 引入了两个省电功能,可通过管理应用在设备未连接至电源时的行为方式为用户延长电池寿命。低电耗模式通过在设备长时间处于闲置状态时推迟应用的后台 CPU 和网络 Activity 来减少电池消耗。应用待机模式可推迟用户近期未与之交互的应用的后台网络 Activity。
低电耗模式和应用待机模式管理在 Android 6.0 或更高版本上运行的所有应用的行为,无论它们是否特别针对 API 级别 23。 为确保用户获得最佳体验,请在低电耗模式和应用待机模式下测试您的应用并对代码进行必要的调整。
需要注意的几点:
- Doze Mode会限制后台应用的cpu、网络。在安卓早期,当内存不够用时,安卓系统会回收后台应用的进程,这仅仅是从 内存占用 层面来限制后台应用。那么现在 系统会进一步限制后台应用的 cpu、网络
- 不论app的targetApi是否23(即安卓6.0),只要用户的手机是基于android 6.0,那么你的应用也将受到Doze Mode的限制。在早期的安卓版本适配时,可能由于时间关系,我们并不会立即适配6.0的权限系统,但为了让应用在6.0上也能正常运行,我们会将app的targetApi 设置5.0,这样app在6.0上运行的效果和在5.0上完全一致。 但这套方式对于Doze Mode不再适用了,可能是google为了回快android新版本的更新速度吧。 毕竟国内开发者的大环境就这样,总要有个鞭子放在脑壳驱动你,你才会去更新。
Doze Mode的两个具体应用:低电耗模式、应用待机模式
这两个模式都是通过 Doze Mode 来实现,那么至些我们可以认为 Doze Mode 只是一种技术手段的名词。
- 低电耗模式:如果用户设备未插接电源、处于静止状态一段时间且屏幕关闭,设备会进入低电耗模式。 在低电耗模式下,系统会尝试通过限制应用对网络和 CPU 密集型服务的访问来节省电量。 这还可以阻止应用访问网络并推迟其作业、同步和标准闹铃。 系统会定期退出低电耗模式一会儿,好让应用完成其已推迟的 Activity。在此维护时段内,系统会运行所有待定同步、作业和闹铃并允许应用访问网络。
- 应用待机模式:系统判定应用在用户未主动使用的进程,都认为此进程处于空闲状态。当用户将设备插入电源时,系统将从待机状态释放应用,也就不会使用Doze Mode来限制后台进程的硬件资源。 Doze和App Standby的区别: Doze模式需要屏幕关闭(通常晚上睡觉或长时间屏幕关闭才会进入),而App Standby不需要屏幕关闭,App进入后台一段时间也会受到连接网络等限制。
在安卓6.0及以后系统上,可以防止 doze mode 让应用进程 进程阻塞挂起状态 的保活方法:
- 启动前台Service
- 自定义锁屏
- 在应用内播放一段无声的音乐这个方法,实测很有效,系统认为如果你的应用进程在锁屏时存在能被用户感知到的行为,那么系统不会阻塞这个进程,播放音乐就是一个能被用户感觉的行为,只不过我们巧妙的循环播放一个无声音乐来欺骗 系统。缺点:可能会多消耗一定的电量,但实测并没有多消耗很多, 半个小时也就消耗了2%电量, 实际就算不播放无声音乐半小时系统也会存在一定的消耗。 所以暂时不需要考虑这方面。
- 保持屏幕长亮(只在某些特殊功能场景下适用,如导航软件)。系统不锁屏,就不会进入doze mode
电池相关
前言
本文主要围绕如下问题进行知识收集整理:
待机、睡眠与休眠的区别? Android开发者官网当中提到“idle states”,该如何理解,这个状态会对设备及我们的程序造成何种影响? 进入Doze模式中的idle状态,我们的程序还能运行吗? 手机睡眠之后,为何我们写Alarm程序、来电显示程序依旧会生效?
如果你也有以上疑问,那么本文会对你解开疑惑有一定的帮助
ACPI简介
要理解第一个问题,得先从ACPI(高级配置与电源接口)说起,ACPI是一种规范(包含软件与硬件),用来供操作系统应用程序管理所有电源接口。 ACPI将计算机系统的状态划分为四个全局状态(G0-G3),共7个状态,其中G0对应S0;G1将低功耗状态细分为四个状态,对应S1-S4;G2、G3代表关机状态分别对应S5、S6。 |ACPI| State Description| |—|—| |S0|正常工作状态| |S1|CPU与RAM供电正常,但CPU不执行指令| |S2|比S1更深的一个睡眠层次,这种模式通常不采用| |S3|挂起到内存| |S4|挂起到硬盘| |S5|Soft Off,CPU、外设等断电,但电源依旧会为部分极低耗设备供电| |S6|Mechanical Off,全部断电|
这里只需要对ACPI的七个状态有个大致了解即可,下一节会有具体的例子来说明各个状态。
Linux系统电源状态
在Linux操作系统中,将电源划分为如下几个状态: |ACPI State| Linux State|Description| |—|—|—| |S0|On(on)|Working| |S1|Standby(standby)|CPU and RAM are powered but not executed| |S2|——|——| |S3|Suspend to RAM(mem)|CPU is Off,RAM is powered and the running content is saved to RAM| |S4|Suspend to Disk(disk)|All content is saved to Disk and power down| |S5|Shutdown|Shutdown the system|
On:正常工作状态
STR(Suspend to RAM)
挂起到内存,俗称待机、睡眠(Sleep),进入该状态,系统的主要工作如下:
- 将系统当前的运行状态等数据保存在内存中,此时仍需要向RAM供电,以保证后续快速恢复至工作状态
- 冻结用户态的进程和内核态的任务(进入内核态的进程或内核自己的task)
- 关闭外围设备,如显示屏、鼠标等,中断唤醒外设不会关闭,如电源键
- CPU停止工作 Standby也属于睡眠的一种方式,属于浅睡眠。该模式下CPU并未断电,依旧可以接收处理某些特定事件,视具体设备而定,恢复至正常工作状态的速度也比STR更快,但也更为耗电。举个例子来说,以该方式进入睡眠时,后续通过点击键盘也能将系统唤醒。而以mem进入的睡眠为深度睡眠,只能通过中断唤醒设备唤醒系统,如电源键(此时按电源键,不会经过正常的开机流程的BIOS、BOOTLOAD等),此时按键盘是无法唤醒系统的。
STD(Suspend to Disk):
挂起到硬盘,俗称休眠(Hibernation)将系统当前的运行状态等数据保存到硬盘上,并自动关机。下次开机时便从硬盘上读取之前保存的数据,恢复到休眠关机之前的状态。 譬如在休眠关机时,桌面打开了一个应用,那么下一次开机启动时,该应用也处于打开状态。而正常的关机-开机流程,该应用是不会打开的。 Linux内核代码声明如下,位于kernel/power/suspend.c
在新版内核中,进程freeze的功能被单独抽离出来作为一个电源状态,该状态仅仅是冻结进程,并不会使系统进入低功耗状态(如切断CPU时钟源、关闭外设供电等)。 相关宏定义位于:linux/include/linux/suspend.h
其中状态4就是STD,所谓的休眠状态(Hibernation)
小结: 至此,我们可以知道,睡眠与休眠是2个不同的概念,睡眠属于STR,而休眠属于STD,切勿混为一谈。 网上也有很多关于“Android休眠”的文章,事实上,Android手机压根儿就不支持休眠模式。
查看Linux支持的电源模式
1 | #查看系统支持的电源模式 |
看来Ubuntu-17.0.4版本是不支持休眠功能了,state当中并没有disk,执行休眠命令也提示找不到。 在公司测试Ubuntu-16.0.4是支持休眠的,休眠时会将当前RAM中的数据保持至swap分区,以供后续恢复。
查看Android支持的电源模式
1 | adb shell |
这里我使用的是模拟器查看的,真机也一样,Android手机是不支持休眠模式的,休眠模式需要一块与RAM大小一致存储空间,这在移动设备上可是个不小的开销。
Idle State
Android上的Idle状态分为二类:Cpu Idle和Device Idle
Cpu Idle
Linux系统运行的基础是基于进程调度,实际上内核调度的线程(task),内核并不会区分线程与进程,都将他们当做一个线程(task)来处理;当所有的进程都没事儿干的时候,系统就会启用idle进程,使系统进入低功耗状态(如关闭一些服务、模块功能,降低CPU工作频率等),即idle状态,以达到省电的目的。 idle状态又可以划分为不同的层级,以MTK的芯片为例,通常划分为以下几个状态: |状态|描述| |soidle(screen on idle)|亮屏 Idle 模式,该模式下与正常工作状态差别不大,唯一的区别就cpu处于空闲状态| |rgidle|浅度 Idle 模式,cpu处于 WFI(wait for interrupt),屏幕熄灭,同时关闭一些不需要的服务及模块,注意此状态cpu的时钟源与RTC模块是工作正常的,此时是可以通过TimerTask的定时触发激活系统的,TimerTask依赖于CPU的RTC模块,而Alarm则依赖于PMIC的RTC模块| |dpidle(deep idle)|深度idle模式,该模式下cpu的时钟源和hrtimer(高精度定时器模块(RTC))被关闭,所有进程(包括系统进程)被冻结,即进入上文所述的睡眠状态|
idle进程是由原始进程(pid=0)在初始化init进程(pid=1)之后演变而来,可以说是init进程的祖先,关于其详细介绍可参考如下链接: Linux Idle基础 魅族内核团队:CPUIDLE 之低功耗定时器
Device Idle
Device Idle属于Doze模式中概念,即指当手机屏幕熄屏、不充电、静置不动,有网友分析了源码,指出6.0手机需要静置1时4分30秒才能进入Doze模式。 Doze模式的限制
- 网络接入被暂停
- 系统忽略wake locks
- 标准的AlarmManager alarms(包括setExact()和setWindow())被延缓到下一个maintenance window
- 如果你需要在Doze状态下启动设置的alarms,使用setAndAllowWhileIdle()或者setExactAndAllowWhileIdle()。当有setAlarmClock()的alarms启动时,系统会短暂退出Doze模式
- 系统不会扫描Wi-Fi
- 系统不允许sync adapters运行
- 系统不允许JobScheduler运行 结合上文分析的cpu idle不难发现Doze模式中的idle状态在概念属于浅idle状态,只是关闭了一些特定服务和模块,并非立即进入睡眠,当然这个过程当中依旧有可能满足睡眠条件而进入睡眠状态,至于如何进入请参考下文【睡眠触发入口】一节。
Android电源管理框架
Android采用linux内核,所以电源状态整体上是与linux操作系统相同,下图是Android的电源管理框架:
WakeLock
唤醒锁,一种锁机制,用于阻止系统进入睡眠状态,只要有应用获取到改锁,那么系统就无法进入睡眠状态。 该机制起初是早期Android为Linux内核打得一个补丁,并想合入到linux内核,但被Linux社区拒绝,后续Linux内核引入自己的Wakelock机制,Android系统也使用的是linux的Wakelock机制,所以该机制并非Android特有的机制。 Android系统提供了两种类型的锁,每一个类型又可分为超时锁与普通锁,超时锁,超时会自动释放,而普通锁则必需要手动释放: |类型|描述| |—|—| |WAKE_LOCK_SUSPEND|阻止系统进入睡眠状态(STR)| |WAKE_LOCK_IDLE|阻止系统从idle进程进入那些具有较大中断时延、禁用了较多中断源的低功耗状态(睡眠除外),持有该类型的锁,不影响系统进入睡眠状态。自Android API-17(对应android linux内核版本3.4)移除了该类型的唤醒锁。| 中断时延:计算机接收到中断信号到操作系统作出响应,并完成转入中断服务程序(ISR)的时间。 内核当中关于WakeLock的主要源码位于: kernel_common/include/linux/wakelock.h kernel_common/kernel/power/wakelock.c
应用层提供的锁类型如下,这些锁都需要手动释放:
FLAG | CPU | 屏幕 | 键盘 |
---|---|---|---|
PARTIAL_WAKE_LOCK | 开启 | 关闭 | 关闭 |
SCREEN_DIM_WAKE_LOCK | 开启 | 变暗 | 关闭 |
SCREEN_BRIGHT_WAKE_LOCK | 开启 | 变亮 | 关闭 |
FULL_WAKE_LOCK | 开启 | 变亮 | 变亮 |
锁的释放 | |||
Linux3.4内核中摒弃了之前的wakelock机制,引入wakeup source机制来进行睡眠管理,为了保证上层接口不变,Android的Linux内核便将wakeup source包装成wakelock,WakeLock的数据结构如下: | |||
当我们应用层释放锁之后,它并不会马上消失。wakelock分为激活和非激活状态,非激活状态300S之内,无人在申请wakelock,那么它将从红黑二叉树,LRU链表当中删除,如此便可复用锁,节省系统开销。 | |||
睡眠触发入口 | |||
在wakelock中,有3个地方可以让系统从early_suspend进入suspend状态。 | |||
1. wake_unlock,系统每释放一个锁,就会检查是否还存其他激活的wakelock,若不存在则执行Linux的标准suspend流程进入睡眠状态 | |||
在超时锁的超时回调函数,判断是否存在其他激活的wakelock,若不存在,则进入睡眠状态 | |||
1. autosleep机制,android 4.1引入该机制,亮屏时会向autosleep节点写入off,熄屏则会写入mem。Android一灭屏,就会尝试进入睡眠,失败之后系统处于idle进程超过一定时间,则又尝试进入睡眠,判断标准同上,若存在wakelock则进入失败 |
关于autosleep机制的内核源码分析,可以参考如下文章: Android autosleep机制
Early Suspend
预挂起机制是Android特有的挂起机制, 这个机制作用是关闭一些与显示相关的外设,比如LCD背光、重力感应器、 触摸屏,但是其他外设如WIFI、蓝牙等模块等并未关闭。 此时,系统依旧可以处理事件,如音乐播放软件,息屏后依旧能播放音乐。 需要注意的是Early Suspend机制与WakeLock机制相互独立,就算有应用持有wakelock锁,系统依旧可以通过Early Suspend机制关闭与显示相关的外设。 注意: Android 4.4起,也就是引入ART的版本,摒弃了early suspend机制,改用了fb event通知机制,即后续版本只有suspend、resume以及runtime suspend、runtime resume。
Late Resume
迟唤醒机制,用于唤醒预挂起的设备
睡眠状态转换
一般情况下,当我们息屏后,系统将先通过Early Suspend机制进入Idle状态,如果满足进入睡眠的条件(没有进程持有唤醒锁)则会通过Linux的Suspend机制进入Sleep(睡眠)状态。
内核源码流程分析可参考如下文章: 源码位于kernel_common/kernel/power/main.c: Android中休眠与唤醒之wake_lock, early_suspend, late_resume
看到这儿,不知你是否疑问,既然系统睡眠了,CPU断电不执行指令了,为何我们定的Alarm会生效以及能接收到来电?
手机来电与Alarm为何能唤醒系统
原来Android在硬件架构上将处理器分为二类:Application Processor(AP)和Baseband Processor(BP),AP是ARM架构的处理器,用于运行Linux+Android系统,耗电量高;BP用于运行实时操作系统(RTOS),用于处理手机通信,耗电量低。 当AP进入睡眠,有来电时,Modem(调制解调器)将唤醒AP;而我们平时所用的Alarm在硬件上则是依赖PMIC(电源管理芯片)中的RTC模块,所以即使AP断电进入睡眠,我们定的闹钟依旧会生效。 若想更深入的了解,则可参考Android RIL机制相关的文章。
总结
- 待机、睡眠与休眠的区别 实际上待机(standby)与睡眠(mem)属于不同模式,但现在大多操作系统都不支持待机模式了,我们也习惯将待机等同于睡眠,睡眠属于STR,休眠属于STD,Android手机不支持休眠!!!
- Android开发者官网当中提到“idle state”,该如何理解,这个状态会对设备及我们的程序造成何种影响 所谓的idle状态,就是指系统进入某个低功耗状态,以MTK为例,常见的状态有soidle、rgidle以及dpidle。rgidle只是限制我们程序使用某些模块,如Doze模式中不能访问网络;而dpidle则会冻结所有进程,系统进入睡眠。
- 进入Doze模式中的idle状态,我们的程序还能运行吗? Doze模式中的idle概念上属于rgidle状态,此时我们的程序是能运行的,只是不能访问网络等,但是在这个过程中,系统可能会满足进入睡眠条件,冻结所有进程,这样我们的程序就不会得到执行。 可以自己写个死循环的线程(普通线程,非looper线程),强制手机进入Doze的idle模式,你会发现你的程序依旧在执行,但是静置在哪儿一段时间后,你会发现你的线程被冻结,不会执行,当你点亮屏幕,你的线程又会继续工作。
- 手机睡眠之后,为何我们写Alarm程序、来电显示程序依旧会生效? Android在硬件架构上将处理器分为AP与BP,应用程序运行与AP之中,睡眠只是将AP断电,BP(Modem)不会断电,当有来电时,BP将会唤醒AP。 Alarm在硬件上依赖的是Modem中的PMIC的RTC模块,而不是AP中的RTC模块,当定时器触发时,可以唤醒AP,使我们的Alarm程序依旧会得到执行
转自 Background optimizations 对低电耗模式和应用待机模式进行针对性优化 Doze mode not affected by a running foreground service associated with a notification while holding a partial wake lock
Fighting with Doze, App Standby and Audio Streaming
Doze Mode/App Standby During Audio Playback Power management restrictions
tips-android-jobscheduler
介绍
JobScheduler是在Android 5.0添加的,它可以检测网络状态、设备是否充电中、低电量、低存储等状态,当所有条件都满足时就会触发执行对应的JobService来完成任务。同时具备了重试、定时执行、持久化任务(设备重启后可恢复任务)等功能。可谓是十分强大 JobScheduler的使用大致分为三步:
- 创建JobService类
- 创建一个JobInfo
- 获取JobScheduler系统服务执行任务
1 创建JobService
JobScheduler的原理大致是是通过监听手机状态,当条件满足时启动Service执行任务。 因此在使用JobScheduler之前,我们需要定义一个Service,当然并不是随随便便定义,而是需要派生自JobService。 只需要重写onStopJob和onStartJob即可。
1 | class DemoJobService: JobService() { |
同时,在manifest中注册时,还需要设置一个权限(否则会报错),如下
1 | <application |
2 创建JobInfo
JobInfo是对任务的描述,比如说需要监听哪些状态、重试策略、任务执行时间、是否持久化等等。 JobInfo.Builder的构造函数需要传入一个jobId,是Job的唯一标志,后续通过该jobId来取消Job。 通过Builder模式构造JobInfo。
1 | val jobInfo = JobInfo.Builder(1, ComponentName(packageName, DemoJobService::class.java.name)) |
JobInfo其他的设置方法:
1 | .setMinimumLatency(5000)//5秒 最小延时、 |
3 执行任务
最后通过getSystemService获取JobScheduler服务执行任务就可以了
1 | //执行任务 |
4 最后
JobScheduler上手还是比较简单的,由于本文是基于api23的源码,因此还有很多新功能没有在源码中展示出来,如setRequiresBatteryNotLow(设置Job对电量的要求)、setRequiresStorageNotLow(设置Job对存储空间的要求)等.
特性&适用
特性
- 支持在一个任务上组合多个条件
- 内置条件:设备待机、设备充电和连接网络
- 支持持续的job,这意味着设备重启后,之前被中断的job可以继续执行
- 支持设置job的最后执行期限
- 根据你的配置,可以设置job在后台运行还是在主线程中运行
适用
需要在Android设备满足某种场合才需要去执行处理数据:
- 应用具有可以推迟的非面向用户的工作(定期数据库数据更新)
- 应用具有当插入设备时希望优先执行的工作(充电时才希望执行的工作备份数据)
- 需要访问网络或 Wi-Fi 连接时需要进行的任务(如向服务器拉取内置数据)
- 希望作为一个批次定期运行的许多任务(s)
特征
- Job Scheduler只有在Api21或以上的系统支持。
- Job Scheduler是将多个任务打包在一个场景下执行。
- 在系统重启以后,任务会依然保留在Job Scheduler当中,因此不需要监听系统启动状态重复设定。
- 如果在一定期限内还没有满足特定执行所需情况,Job Scheduler会将这些任务加入队列,并且随后会进行执行。
使用Job Scheduler,应用需要做的事情就是判断哪些任务是不紧急的,可以交给Job Scheduler来处理,Job Scheduler集中处理收到的任务,选择合适的时间,合适的网络,再一起进行执行。把时效性不强的工作丢给它做。
源码分析
JobScheduler是系统服务JobSchedulerService,JobScheduler是一个抽象类;以下是JobScheduler的源码:
1 | public abstract class JobScheduler { |
JobScheduler的实现类是JobSchedulerImpl:
1 | public class JobSchedulerImpl extends JobScheduler { |
在代码中 调用mJobScheduler.schedule(job);其实是调了JobScheduler的实现类JobSchedulerImpl中的schedule方法;然后再调了mBinder.schedule(job);这个mBinder就是JobSchedulerService,调用了JobSchedulerService类里面IJobScheduler.Stub内部类的schedule方法; JobSchedulerService是在哪里启动的呢?先看一下的源码,从源码分析JobSchedulerService是一个系统服务;
1 | public class JobSchedulerService extends com.android.server.SystemService |
是系统服务那应该就是在 SystemServer启动的,先看一下SystemServer的源码;
1 | public final class SystemServer { |
手机开机启动会走SystemServer中的主函数main方法,main方法调用了run方法,下面是run方法中的代码:
1 | private void run() { |
从run方法可以看出,这里启动了一系列的系统服务,里面调用了startOtherServices()方法,那接下看一下startOtherServices方法,向下看:
1 | private void startOtherServices() { |
在startOtherServices方法方法中调用了mSystemServiceManager.startService(JobSchedulerService.class);这里就启动了JobSchedulerService服务;接下来我们分析JobSchedulerService的源码; 先看一下JobSchedulerService的构造方法:
1 | public JobSchedulerService(Context context) { |
创建了5个不同的StateController,分别添加到mControllers。 |类型|说明| |—|—| |ConnectivityController| 注册监听网络连接状态的广播| |TimeController| 注册监听job时间到期的广播| |IdleController| 注册监听屏幕亮/灭,dream进入/退出,状态改变的广播| |BatteryController| 注册监听电池是否充电,电量状态的广播| |AppIdleController| 监听app是否空闲|
前面提到调用mBinder.schedule(job);其实是调用了JobSchedulerService类里面IJobScheduler.Stub内部类的schedule方法;接下看一下这个方法:
1 | final class JobSchedulerStub extends IJobScheduler.Stub { |
从上面代码看出,是调用了调用JobSchedulerService中的schedule方法,好了,看一下JobSchedulerService中的schedule方法;
1 | public int schedule(JobInfo job, int uId) { |
从上面代码可以看出是通过Handler发消息,MSG_CHECK_JOB是目标源,看一下JobHandler 中的方法
1 | private class JobHandler extends Handler { |
通过Handler发消息,然后调用 maybeQueueReadyJobsForExecutionLockedH()方法,
1 | private void queueReadyJobsForExecutionLockedH() { |
这个方法主要是遍历将来要处理的工作任务然后一个个加到待处理工作任务集合中去;这个方法执行完后就会执行JobHandler中的maybeRunPendingJobsH()方法;
1 | private void maybeRunPendingJobsH() { |
从上面源码分析可以得出,这个方法通过遍历待处理任务集合,处理任务,这里调用了availableContext.executeRunnableJob(nextPending)方法,这个就是处理待处理任务的方法,接下来我们一起看看这个方法的源码,分析下:
1 | public class JobServiceContext extends IJobCallback.Stub implements ServiceConnection { |
JobServiceContext是ServiceConnection,这个是进程间通讯ServiceConnection,通过调用availableContext.executeRunnableJob(nextPending)方法,会触发调用onServiceConnected,看到这里应该明白了,onServiceConnected方法中的service就是jobservice,里面还用了WakeLock锁,防止手机休眠,然后通过Handler发消息 调用了handleServiceBoundH()方法,
1 | /** Start the job on the service. */ |
从上面源码可以看出,最终是调用了jobService中的startjob方法, 这样就明白了,是如何触发调用jobService中的startjob方法的;
前面在JobSchedulerService中提到了控件类StateController类,这个是一个抽象类,有很多实现类,这个我只分析一个ConnectivityController实现类,其他都差不多,接下来分析一下ConnectivityController源码:
1 | public class ConnectivityController extends StateController implements |
上面是网络联接控制类ConnectivityController,当网络发生改变时,会触发网络连接改变广播,然后调用updateTrackedJobs(userid)方法,在updateTrackedJobs方法中,会判断网络是否有改变,有改变的会调 mStateChangedListener.onControllerStateChanged()方法;这样又调用了JobSchedulerService类中onControllerStateChanged方法:
1 | @Override |
在onControllerStateChanged方法中通过handler发消息,然后调用了maybeQueueReadyJobsForExecutionLockedH();
1 | private void maybeQueueReadyJobsForExecutionLockedH() { |
通过上面方法,把待处理的工作任务加到集合中,然后再调 maybeRunPendingJobsH();这个前面已提到过,就不再说了,一样的;
I/O多路复用
Unix五种IO模型,不了解的可以 => 查看这里
- blocking IO - 阻塞IO
- nonblocking IO - 非阻塞IO
- IO multiplexing - IO多路复用
- signal driven IO - 信号驱动IO
- asynchronous IO - 异步IO
I/O multiplexing 也就是我们所说的I/O多路复用,但是这个翻译真的很不生动,所以我更喜欢将它拆开,变成 I/O multi plexing
select总结: select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
- 单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
- 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
- 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
python服务器程序部署
一、介绍
WSGI(Web Server Gateway Interface),翻译为Python web服务器网关接口,即Python的Web应用程序(如Flask)和Web服务器(如Nginx)之间的一种通信协议。也就是说,如果让你的Web应用在任何服务器上运行,就必须遵循这个协议。 那么实现WSGI协议的web服务器有哪些呢?就比如uWSGI与gunicorn。两者都可以作为Web服务器。可能你在许多地方看到的都是采用Nginx + uWSGI(或gunicorn)的部署方式。实际上,直接通过uWSGI或gunicorn直接部署也是可以让外网访问的,那你可能会说,那要Nginx何用?别急,那么接来下介绍另一个Web服务器——Nginx Nginx作为一个高性能Web服务器,具有负载均衡、拦截静态请求、高并发…等等许多功能,你可能要问了,这些功能和使用Nginx + WSGI容器的部署方式有什么关系? 首先是负载均衡,如果你了解过OSI模型的话,其实负载均衡器就是该模型中4~7层交换机中的一种,它的作用是能够仅通过一个前端唯一的URL访问分发到后台的多个服务器,这对于并发量非常大的企业级Web站点非常有效。在实际应用中我们通常会让Nginx监听(绑定)80端口,通过多域名或者多个location分发到不同的后端应用。 其次是拦截静态请求,简单来说,Nginx会拦截到静态请求(静态文件,如图片),并交给自己处理。而动态请求内容将会通过WSGI容器交给Web应用处理; Nginx还有其他很多的功能,这里便不一一介绍。那么前面说了,直接通过uWSGI或gunicorn也可以让外网访问到的,但是鉴于Nginx具有高性能、高并发、静态文件缓存、及以上两点、甚至还可以做到限流与访问控制,所以选择Nginx是很有必要的; 这里可以说明,如果你选择的架构是:Nginx + WSGI容器 + web应用,WSGI容器相当于一个中间件;如果选择的架构是uWSGI + web应用,WSGI容器则为一个web服务器
二、实际部署:
普遍的部署方式都是通过让Nginx绑定80端口,并接受客户端的请求将动态内容的请求反向代理给运行在本地端口的uWSGI或者Gunicorn,所以既可以通过Nginx + uWSGI也可以通过Nginx + Gunicorn来部署Flask应用,这篇教程中都将一一介绍这两种方法 当然采用不同的WSGI容器,Nginx中的配置也会有所不同
1. Nginx + uWSGI
1.1 配置uWSGI:
我们现在虚拟环境下安装好uWSGI:
1 | pip install uwsgi |
安装完成之后我们在项目的目录下(即你实际创建的Flask项目目录,在本文所指的项目目录都假设为/www/demo)创建以.ini为扩展名的配置文件。在设置与Nginx交互的时候有两种方式: 第一种是通过配置网络地址,第二种是通过本地的.socket文件进行通信。需要注意的是,不同的交互方式下,Nginx中的配置也会有所不同 如果采用的是第一种网络地址的方式,则将之前创建uwsgi.ini配置文件添加如下的配置内容:
1 | [uwsgi] |
这里的wsgi-file参数所指的run.py其实是启动文件,你也可以使用manage.py。不过我通常习惯创建一个这样的文件,可以直接运行该文件来启动项目:
1 | from app import app |
保存好配置文件后,就可以通过如下的命令来启动应用了:
1 | uwsgi uwsig.ini |
如果你采用的是第二种本地socket文件的方式,则添加如下的配置内容:
1 | [uwsgi] |
可以看到,其实与网络地址的配置方式只有socket参数的配置不同,在这里填写好路径名和文件名并启动uWSGI后,将会自动在改目录下生成nginx_uwsgi.socket文件,这个文件就是用来与Nginx交互的。
1.2 配置Nginx
首先我们来通过apt安装Nginx:
1 | sudo apt-get install nginx |
安装完成之后,我们cd到/etc/nginx/的目录下(可能由于不同系统导致不同的Nginx发行版缘故,目录有所差别,在此只针对Ubuntu中的发行版的Nginx),可以看到Nginx的所有配置文件。 其中nginx.conf文件为主配置文件,可以用来修改其全局配置;sites-available存放你的配置文件,但是在这里添加配置是不会应用到Nginx的配置当中,需要软连接到同目录下的sites-enabled当中。但是在我实际操作的过程中中,当我在sites-available修改好配置文件后,会自动更新到sites-enabled。如果没有的话,则需要像上述的操作那样,将修改好的配置文件软链接到sites-enabled当中 在上边说到,配置uWSGI有两种与Nginx交互的方式,那么选择不同的方式的话在Nginx的配置也会有所不同:
第一种:网络配置方式:
这里的proxy_set_header设置的三个参数的作用都是能够直接获得到客户端的IP,如果你感兴趣可以参考:Nginx中proxy_set_header 理解 用include uwsgi_params导入uWSGI所引用的参数,通过uwsgi_pass反向代理给在localhost:8001运行的uWSGI:
1 | server { |
在每次完Nginx配置文件内容后,需要通过如下的命令来重启Nginx:
1 | nginx -s reload |
第二种:socket文件方式:
与上边的配置内容大体相同,只是在配置uwsgi_pass不是反向代理给网络地址,而是通过socket文件进行交互,我们只需要指定之前设置的路径和文件名即可:
1 | server { |
2. Nginx + Gunicorn
2.1 配置Gunicorn:
首先先在虚拟环境下安装Gunicorn:
1 | pip install gunicorn |
安装完成后,我们来创建以.py结尾的配置文件,这里我参考了Jiyuankai的GitHub关于Gunicorn的配置文件内容:
1 | from gevent import monkey |
需要注意的是要在配置文件的同层目录下创建log文件,否则运行gunicorn将报错。添加完配置内容并保存为gconfig.py文件后,我们就也可以通过gunicorn来运行Flask应用了:
1 | gunicorn -c /www/demo/gconfig.py run:app |
2.2 配置Nginx:
和uWSGI的任意一种配置方法类似,只是在location中的配置有所不同:
1 | server { |
通过Gunicorn的Nginx配置中,我们只需要通过proxy_pass参数反向代理给运行在http://localhost:5000/上的Gunicorn
三、守护进程
如果你采取如上的任意一种部署方式,在Nginx与uWSGI或Gunicorn同时运行,并且配置无误的状态下,那么你现在应该是可以通过你的公网ip或者域名访问到你的网站了。 但是还有一个问题,到目前为止,uWSGI和gunicorn都是直接通过命令行运行,并不能够在后台运行,也是当我们关闭了xShell(或者你使用的是Putty及其他SSH连接的软件),将无法再访问到你的应用。所以我们需要让uWSGI或gunicorn在后台运行,也就是所谓的daemon(守护进程)。
1. nohup:
如果你熟悉Linux命令,你应该知道在Linux中后台运行可以通过nohup命令,例如我们要让gunicorn在后台运行,我们只需要运行nohup命令:
1 | nohup gunicorn -c gconfig.py run:app & |
运行后你可以通过ps -e | grep gunicorn指令来查看到当前gunicorn的运行状态: 如果你选择的是uWSGI,同样也可以通过nohup命令来实现守护进程:
1 | nohup uwsgi uwsgi.ini & |
这样你就可以关闭连接服务器的终端,还能通过你的浏览器访问到你的Flask应用了!
2. supervisor
但是nohup运行的后台程序并不能够可靠的在后台运行,我们最好让我们的后台程序能够监控进程状态,还能在意外结束时自动重启,这就可以使用一个使用Python开发的进程管理程序supervisor。 参考:https://www.cnblogs.com/Dicky-Zhang/p/6171954.html 首先我们通过apt来安装supervisor:
1 | apt-get install supervisor |
安装完成后,我们在/etc/supervisor/conf.d/目录下创建我们控制进程的配置文件,并以.conf结尾,这样将会自动应用到主配置文件当中,创建后添加如下的配置内容:
1 | [program:demo] |
在上面的配置文件中,[program:demo]设置了进程名,这与之后操作进程的状态名称有关,为demo;command为进程运行的命令,必须使用绝对路径,并且使用虚拟环境下的gunicorn命令;user指定了运行进程的用户,这里设置为root 保存配置文件之后,我们需要通过命令来更新配置文件:
1 | supervisorctl update |
命令行将显示:demo: added process group,然后我们来启动这个demo进程:
1 | supervisorctl start demo |
当然你也直接在命令行输入supervisorctl进入supevisor的客户端,查看到当前的进程状态:
1 | demo RUNNING pid 17278, uptime 0:08:51 |
通过stop命令便可以方便的停止该进程:
1 | supervisor> stop demo |
db-mysql
CentOS环境
CentOS 7的默认yum仓库中并没有MySQL5.7,我们需要手动添加,好在MySQL官方提供了仓库的地址,所以我们能够比较简单地安装MySQL。如下是CentOS 7下MySQL5.7的安装。
安装
1.添加Mysql5.7仓库
1 | sudo rpm -ivh https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm |
2.确认Mysql仓库成功添加
1 | sudo yum repolist all | grep mysql | grep enabled |
如果展示像下面,则表示成功添加仓库:
1 | mysql-connectors-community/x86_64 MySQL Connectors Community enabled: 51 |
3.开始安装Mysql5.7
1 | sudo yum -y install mysql-community-server |
4.启动Mysql
- 启动:
sudo systemctl start mysqld
- 设置系统启动时自动启动:
systemctl enable mysqld
- 查看启动状态:
systemctl status mysqld
5.Mysql的安全设置
CentOS上的root默认密码可以在文件/var/log/mysqld.log
找到,通过下面命令可以打印出来1
cat /var/log/mysqld.log | grep -i 'temporary password'
执行下面命令进行安全设置,这个命令会进行设置root密码设置,移除匿名用户,禁止root用户远程连接等
1 | mysql_secure_installation |
6.设置数据库编码为utf8
在 /etc/my.cnf
的[mysqld],[client],[mysql]
节点下添加编码设置
1 | [client] |
重启Mysql:systemctl restart mysqld
重新安装
1 | yum remove mysql mysql-server mysql-libs mysql-server; |
查看是否还有mysql软件:
1 | rpm -qa|grep mysql |
如果存在的话,继续删除即可。 在MySQL官网中下载YUM源rpm安装包:http://dev.mysql.com/downloads/repo/yum/
1 | # 下载mysql源安装包 |