前言
回来啦,《Android面试题思考与解答21年3月刊》送给大家。
介绍
《面试题思考与解答》系列期刊是将每月的知识点进行总结汇总。
要声明的一点是:面试题的目的不是为了让大家背题,而是从不同维度帮助大家复习
,取长补短。
希望大家都能找到满意的工作。
以下为2021年3月刊内容。
Activity、PhoneWindow、DecorView、ViewRootImpl 之间的关系?
PhoneWindow
:是Activity和View交互的中间层,帮助Activity管理View。DecorView
:是所有View的最顶层View,是所有View的parent。ViewRootImpl
:用于处理View相关的事件,比如绘制,事件分发,也是DecorView的parent。
四者的创建时机?
Activity
创建于performLaunchActivity方法中,在startActivity时候触发。PhoneWindow
,同样创建于performLaunchActivity方法中,再具体点就是Activity的attach方法。DecorView
,创建于setContentView->PhoneWindow.installDecor。ViewRootImpl
,创建于handleResumeActivity方法中,最后通过addView被创建。
View的第一次绘制发生在什么时候?
第一次绘制就是发生在handleResumeActivity
方法中,通过addView方法,创建了ViewRootImpl
,并调用了其setView
方法。
最后调用到requestLayout方法开始了布局、测量、绘制的流程。
线程更新UI导致崩溃的原因?
在触发绘制方法requestLayout中,有个checkThread方法:
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
其中对mThread和当前线程进行了比较。而mThread是在ViewRootImpl
实例化的时候赋值的。
所以崩溃的原因就是 view被绘制到界面时候的线程(也就是ViewRootImpl被创建时候的线程)和进行UI更新时候的线程不是同一个线程。
Activity、Dialog、PopupWindow、Toast 与Window的关系
这是扩展的一题,简单的从创建方式
的角度来说一说:
Activity
。在Activity创建过程中所创建的PhoneWindow,是层级最小的Window,叫做应用Window
,层级范围1-99
。(层级范围大的Window可以覆盖层级小的Window)Dialog
。Dialog的显示过程和Activity
基本相同,也是创建了PhoneWindow,初始化DecorView,并将Dialog的视图添加到DecorView中,最终通过addView显示出来。
但是有一点不同的是,Dialog的Window并不是应用窗口,而是子窗口
,层级范围1000-1999
,子Window的显示必须依附于应用窗口,也会覆盖应用级Window。这也就是为什么Dialog传入的上下文必须为Activity的Context了。
PopupWindow
。PopupWindow的显示就有所不同了,它没有创建PhoneWindow,而是直接创建了一个View(PopupDecorView)
,然后通过WindowManager的addView方法显示出来了。
没有创建PhoneWindow,是不是就跟Window没关系了呢?
并不是,其实只要是调用了WindowManager
的addView
方法,那就是创建了Window
,跟你有没有创建PhoneWindow
无关。View
就是Window
的表现形式,只不过PhoneWindow
的存在让Window
形象更立体了一些。
所以PopupWindow
也是通过Window展示出来的,而它的Window层级属于子Window,必须依附与应用窗口。
Toast
。Toast
和PopupWindow
比较像,没有新建PhoneWindow
,直接通过addView方法显示View即可。不同的是它属于系统级Window
,层级范围2000-2999
,所以无须依附于Activity。
四个比较下来,可以发现,只要想显示View,就会涉及到WindowManager
的addView
方法,也就用到了Window这个概念,然后会根据不同的分层依次显示覆盖到界面上。
不同的是,Activity
和Dialog
涉及到了布局比较复杂,还会有布局主题等元素,所以用到了PhoneWindow进行一个解耦,帮助他们管理View。而PopupWindow
和Toast
结构比较简单,所以直接新建一个类似DecorView的View,通过addView显示到界面。
为什么限制在应用间共享文件
打个比方,应用A有一个文件,绝对路径为file:///storage/emulated/0/Download/photo.jpg
现在应用A想通过其他应用来完成一些需求,比如拍照,就把他的这个文件路径发给了照相应用B,然后应用B照完相就把照片存储到了这个绝对路径。
看起来似乎没有什么问题,但是如果这个应用B是个“坏应用”呢?
- 泄漏了文件路径,也就是应用隐私。
如果这个应用A是“坏应用”呢?
- 自己可以不用申请存储权限,利用应用B就达到了存储文件的这一危险权限。
可以看到,这个之前落伍的方案,从自身到对方,都是不太好的选择。
所以Google就想了一个办法,把对文件的访问限制在应用内部。
- 如果要分享文件路径,不要分享
file:// URI
这种文件的绝对路径,而是分享content:// URI
,这种相对路径,也就是这种格式:content://com.jimu.test.fileprovider/external/photo.jpg
- 然后其他应用可以通过这个绝对路径来向文件所属应用 索要 文件数据,所以文件所属的应用本身必须拥有文件的访问权限。
也就是应用A分享相对路径给应用B,应用B拿着这个相对路径找到应用A,应用A读取文件内容返给应用B。
介绍下FileProvider
涉及到应用间通信的问题,还记得IPC的几种方式吗?
- 文件
- AIDL
- ContentProvider
- Socket
- 等等。
从易用性,安全性,完整度等各个方面考虑,Google选择了ContentProvider
为这次限制应用分享文件的 解决方案。于是,FileProvider
诞生了。
具体做法就是:
<!-- 配置FileProvider--> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="external" path="."/> </paths>
//修改文件URL获取方式 Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", createImageFile());
这样配置之后,就能生成content:// URI
,并且也能通过这个URI
来传输文件内容给外部应用。
FileProvider这些配置属性也就是ContentProvider
的通用配置:
android:name
,是ContentProvider的类路径。android:authorities
,是唯一标示,一般为包名+.providerandroid:exported
,表示该组件是否能被其他应用使用。android:grantUriPermissions
,表示是否允许授权文件的临时访问权限。
其中要注意的是android:exported
正常应该是true,因为要给外部应用使用。
但是FileProvider
这里设置为false,并且必须为false。
这主要为了保护应用隐私,如果设置为true
,那么任何一个应用都可以来访问当前应用的FileProvider
了,对于应用文件来说肯定是不可取的,所以Android7.0
以上会通过其他方式让外部应用安全的访问到这个文件,而不是普通的ContentProvider访问方式,后面会说到。
当然,也正是因为这个属性为true,所以在Android7.0以下,Android默认是将它当成一个普通的ContentProvider
,外部无法通过content:// URI
来访问文件。所以一般要判断下系统版本再确定传入的Uri到底是File格式还是content格式。
Service与子线程
关于Service
,我的第一反应是运行在后台的服务。
关于后台
,我的第一反应又是子线程。
那么Service和子线程到底是什么关系呢?
Service
有两个比较重要的元素:
长时间运行
。Service可以在Activity被销毁,程序被关闭之后都可以继续运行。不提供界面的应用组件
。这其实解释了后台的意义,Service的后台指的是不和界面交互,不依赖UI元素。
而且比较关键的点是,Service
也是运行在主线程之中。
所以运行在后台的Service
和运行在后台的线程区别还是挺大的。
- 首先,所运行的线程不同。
Service
还是运行在主线程,而子线程肯定是开辟了新的线程。 - 其次,后台的概念不同。
Service
的后台指的是不与界面交互,子线程的后台指的是异步运行。 - 最后,
Service
作为四大组件之一,控制它也更方便,只要有上下文就可以对其进行控制。
当然,虽然两者概念不同,但是还是有很多合作之处。
Service
作为后台运行的组件,其实很多时候也会被用来做耗时操作,那运行在主线程的Service肯定不能直接进行耗时操作,这就需要子线程了。
开启一个后台Service
,然后在Service
里面进行子线程操作,这样的结合给项目带来的可能性就更大了。
Google也是考虑到这一点,设计出了IntentService
这种已经结合好的组件供我们使用。
后台和前台Service
这就涉及到Service
的分类了。
如果从是否无感知来分类,Service可以分为前台和后台。前台Service会通过通知的方式让用户感知到,后台有这么一个玩意在运行。
比如音乐类APP,在后台播放音乐的同时,可以发现始终有一个通知显示在前台,让用户知道,后台有一个这么音乐相关的服务。
在Android8.0
,Google要求如果程序在后台,那么就不能创建后台服务,已经开启的后台服务会在一定时间后被停止。
所以,建议使用前台Service
,它拥有更高的优先级,不易被销毁。使用方法如下:
startForegroundService(intent); public void onCreate() { super.onCreate(); Notification notification = new Notification.Builder(this) .setChannelId(CHANNEL_ID) .setContentTitle("主服务")//标题 .setContentText("运行中...")//内容 .setSmallIcon(R.mipmap.ic_launcher) .build(); startForeground(1,notification); } <!--android 9.0上使用前台服务,需要添加权限--> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
那后台任务该怎么办呢?官方建议使用 JobScheduler 。
说说JobScheduler
任务调度JobScheduler
,Android5.0
被推出。(可能有的朋友感觉比较陌生,其实他也是通过Service实现的,这个待会再说)
它能做的工作就是可以在你所规定的要求下进行自动任务执行。比如规定时间、网络为WIFI情况、设备空闲、充电时等各种情况下后台自动运行。
所以Google让它来替代后台Service的一部分功能,使用:
- 首先,创建一个JobService:
public class MyJobService extends JobService { @Override public boolean onStartJob(JobParameters params) { return false; } @Override public boolean onStopJob(JobParameters params) { return false; } }
- 然后,注册这个服务(因为JobService也是Service)
<service android:name=".MyJobService" android:permission="android.permission.BIND_JOB_SERVICE" />
- 最后,创建一个JobInfo并执行
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); ComponentName jobService = new ComponentName(this, MyJobService.class); JobInfo jobInfo = new JobInfo.Builder(ID, jobService) .setMinimumLatency(5000)// 任务最少延迟时间 .setOverrideDeadline(60000)// 任务deadline,当到期没达到指定条件也会开始执行 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)// 网络条件,默认值NETWORK_TYPE_NONE .setRequiresCharging(true)// 是否充电 .setRequiresDeviceIdle(false)// 设备是否空闲 .setPersisted(true) //设备重启后是否继续执行 .setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR) //设置退避/重试策略 .build(); scheduler.schedule(jobInfo);
简单说下原理:
JobSchedulerService
是在SystemServer中启动的服务,然后会遍历没有完成的任务,通过Binder找到对应的JobService,执行onStartJob方法,完成任务。具体可以看看参考链接的分析。
所以也就知道了,在5.0之后,如果有需要后台任务执行,特别是需要满足一定条件触发的任务,比如网络电量等等情况,就可以使用JobScheduler。
有的人可能要问了,5.0之前怎么办呢?
- 可以使用GcmNetworkManager或者BroadcastReceiver等处理部分情况下的任务需求。
Google也是考虑到了这一点,所以将5.0之后的JobScheduler
和5.0之前的GcmNetworkManager、GcmNetworkManager、AlarmManager
等和任务相关的API相结合,设计出了WorkManager
。
说说WorkManager
WorkManager 是一个 API,可供您轻松调度那些即使在退出应用或重启设备后仍应运行的可延期异步任务。
作为Jetpack的一员,并不算很新的内容,它的本质就是结合已有的任务调度相关的API,然后根据版本需求等来执行这些任务,官网有一张图:
所以WorkManager到底能做什么呢?
- 1、对于一些任务约束能很好的执行,比如网络、设备空闲状态、足够存储空间等条件下需要执行的任务。
- 2、可以重复、一次性、稳定的执行任务。包括在设备重启之后都能继续任务。
- 3、可以定义不同工作任务的衔接关系。比如设定一个任务接着一个任务。
总之,它是后台执行任务的一大利器。
onStart可见的解释?onStart和onResume两种状态的设计。
首先,科普官方定义的两个状态。
- onStart到onStop中间的状态叫做
“已开始”
状态。 - onResume到onPause中间的状态叫做
“已恢复”
状态。
然后我们做个小实验,定义ActivityA
和 ActivityB
,ActivityB
为Dialog主题,ActivityA
中点击可以跳转到B:
image.setOnClickListener { startActivity(Intent(this, ActivityB::class.java)) } <activity android:name=".activity.ActivityB" android:theme="@style/Theme.AppCompat.Light.Dialog" android:launchMode="standard"> </activity>
进入ActivityA后,点击按钮,跳转到B,这时候A的生命周期走到了onPause
,也就是回到了已开始
状态。
这个时候,界面是这个样子:
ActivityA处在已开始状态,对用户可见。
这里的可见是不是就很好理解了,确实对我们可见了,只不过 不在前台,不能交互。
所以延伸到普通的Activity
,这个可见,并不是表示用户能用肉眼看到了,而是想表达:
Activity
已经显示出来了,但是还不在前台
,所以只是可见
,但不可交互。
这个可见状态是从onStart开始,onStop结束,我们可以分为两个阶段:
- onStart到onResume。这个阶段,Activity被创建,布局已加载,但是界面还没绘制,可以说界面都不存在。
- onPause到onStop。这个阶段,就是我们刚才所做的实验,Activity有界面,只是被新的界面所遮挡,也就是不在前台。
所以综合两个阶段,我们把这种Activity被创建或已经显示出来,但是不在前台,介于两者之间的状态叫做 可见
状态。
到此,我们知道了可见的意思,其实也就知道了另外一个问题,也就是为什么要设计出onStart和onResume这两种状态。
onStart和onStop
,是从Activity是否可见的角度设计的。onResume和onPause
,是从Activity是否位于前台的角度设计的。
所以Activity
的生命周期又可以解释为:
被创建(onCreate)——> 可见(onStart)——> 位于前台(onResume)——> 可见但不在前台(onPause)