前言
薪资太低欲辞职,面试做题心甚难。 屡屡面试屡遇坎,每日一题快来看。
又要到金九银十的跳槽季了,为了让更多的小伙伴可以在面试的时候取的更好的offer,每日工作之余,我都会将自己整理的一些Android面试题笔试题整理成每日一题,然后在平台上推送给大家,每天仅需几分钟做一道题,经过日积月累,在换工作的时候一定能让你拿到一个比较好的offer。今天这篇文章是我将近期整理出比较好的题目及粉丝们分享的一些答案进行的整理,分享给更多的朋友们,希望可以帮助到你。
要声明的一点是:面试题的目的不是为了让大家背题,而是从不同维度帮助大家复习,取长补短。
希望大家都能找到满意的工作。
1、Activity、PhoneWindow、DecorView、ViewRootImpl 之间的关系?
PhoneWindow
:是Activity和View交互的中间层,帮助Activity管理View。DecorView
:是所有View的最顶层View,是所有View的parent。ViewRootImpl
:用于处理View相关的事件,比如绘制,事件分发,也是DecorView的parent。
2、四者的创建时机?
Activity
创建于performLaunchActivity方法中,在startActivity时候触发。PhoneWindow
,同样创建于performLaunchActivity方法中,再具体点就是Activity的attach方法。DecorView
,创建于setContentView->PhoneWindow.installDecor。ViewRootImpl
,创建于handleResumeActivity方法中,最后通过addView被创建。
3、View的第一次绘制发生在什么时候?
第一次绘制就是发生在handleResumeActivity
方法中,通过addView方法,创建了ViewRootImpl
,并调用了其setView
方法。
最后调用到requestLayout方法开始了布局、测量、绘制的流程。
4、线程更新UI导致崩溃的原因?
在触发绘制方法requestLayout中,有个checkThread方法:
代码语言:javascript复制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更新时候的线程不是同一个线程。
5、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显示到界面。
6、为什么限制在应用间共享文件
打个比方,应用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。
7、介绍下FileProvider
涉及到应用间通信的问题,还记得IPC的几种方式吗?
- 文件
- AIDL
- ContentProvider
- Socket
- 等等。
从易用性,安全性,完整度等各个方面考虑,Google选择了ContentProvider
为这次限制应用分享文件的 解决方案。于是,FileProvider
诞生了。
具体做法就是:
代码语言:javascript复制<!-- 配置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>
代码语言:javascript复制//修改文件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格式。
8、Service与子线程
关于Service
,我的第一反应是运行在后台的服务。
关于后台
,我的第一反应又是子线程。
那么Service和子线程到底是什么关系呢?
Service
有两个比较重要的元素:
长时间运行
。Service可以在Activity被销毁,程序被关闭之后都可以继续运行。不提供界面的应用组件
。这其实解释了后台的意义,Service的后台指的是不和界面交互,不依赖UI元素。
而且比较关键的点是,Service
也是运行在主线程之中。
所以运行在后台的Service
和运行在后台的线程区别还是挺大的。
- 首先,所运行的线程不同。
Service
还是运行在主线程,而子线程肯定是开辟了新的线程。 - 其次,后台的概念不同。
Service
的后台指的是不与界面交互,子线程的后台指的是异步运行。 - 最后,
Service
作为四大组件之一,控制它也更方便,只要有上下文就可以对其进行控制。
当然,虽然两者概念不同,但是还是有很多合作之处。
Service
作为后台运行的组件,其实很多时候也会被用来做耗时操作,那运行在主线程的Service肯定不能直接进行耗时操作,这就需要子线程了。
开启一个后台Service
,然后在Service
里面进行子线程操作,这样的结合给项目带来的可能性就更大了。
Google也是考虑到这一点,设计出了IntentService
这种已经结合好的组件供我们使用。
9、后台和前台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 。
10、说说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
。
11、说说WorkManager
WorkManager 是一个 API,可供您轻松调度那些即使在退出应用或重启设备后仍应运行的可延期异步任务。
作为Jetpack的一员,并不算很新的内容,它的本质就是结合已有的任务调度相关的API,然后根据版本需求等来执行这些任务,官网有一张图:
所以WorkManager到底能做什么呢?
- 1、对于一些任务约束能很好的执行,比如网络、设备空闲状态、足够存储空间等条件下需要执行的任务。
- 2、可以重复、一次性、稳定的执行任务。包括在设备重启之后都能继续任务。
- 3、可以定义不同工作任务的衔接关系。比如设定一个任务接着一个任务。
总之,它是后台执行任务的一大利器。
12、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)
总结
这次整理的这些每日一题都是一些编程题,其中有些还是我们日常开发中可能会遇到的问题,通过做这些题也可以检验一下自己对这些实用编程技巧的掌握程度。每日一题,每天成长一点点。
如果你觉得自己学习效率低,缺乏正确的指导,可以参考下下面分享我多年工作以来收集整理的学习路线,给大家做个参考:
1、确定好方向,梳理成长路线图
不用多说,相信大家都有一个共识:无论什么行业,最牛逼的人肯定是站在金字塔端的人。所以,想做一个牛逼的程序员,那么就要让自己站的更高,成为技术大牛并不是一朝一夕的事情,需要时间的沉淀和技术的积累。
关于这一点,在我当时确立好Android方向时,就已经开始梳理自己的成长路线了,包括技术要怎么系统地去学习,都列得非常详细。
知识梳理完之后,就需要进行查漏补缺,所以针对这些知识点,我手头上也准备了不少的电子书和笔记,这些笔记将各个知识点进行了完美的总结。
2、看视频进行系统学习
前几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。
另外,我自己也珍藏了好几套视频,有需要的我也可以分享给你。
3、通过源码来系统性地学习
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
4、简历准备充分
可以去知乎上面搜搜别人写的有关简历的帖子,怎么准备,吸引hr, 突出自己的优点和能力,或者让朋友帮你看看简历有没有问题,比如过于简单或者浮夸,没有重点等。
尽量把你的亮点总结成一句简练的话,再加上数字来说明它的影响和意义。
其次在简历里中加入了可交互、可展示的内容,更能显出你的能力与众不同。
有很多人经常也会遇到很多关于简历制作,职业困惑、HR经典面试问题回答等有关面试的问题。同样我也搜集整理了全套简历制作、金三银四社招困惑、HR面试等问题解析,有疑问,可以提供专业的解答。
5、刷题备战,直通大厂
面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。
关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三。
以上这些资料都已打包整理完毕,点击我的GitHub可以看到全部内容。或者点击 【这里】 查看获取方式。
不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高