笔者是面霸,面试500+场 当过考官:面过别人500+场 去过500强,也呆过初创公司。
斩获腾讯、华为、字节跳动,蚂蚁金服,OPPO,美团,offer!我有一套速通大厂技巧分享给你!
12年毕业,专科生,做安卓9年。横扫各大互联网公司,从4k工资现在的4万。我经历了什么?
一个一个问题来总结一下:答案由蚂蚁金服首席架构师提供
答:MVC是比较早的一种软件开发模式了。
M对应模型层(Model)针对业务模型,建立的数据结构和相关的类,它主要负责网络请求,数据库处理,I/O的操作。
V代表视图层:对应于xml布局文件和java代码动态view部分。
C代表Controller层对应Activity,用于绑定View及向Model层发送指令获取数据。
MVP中把MVC的C改P,View层和Model层不直接通信,通过实现一个View接口来进行通信,实现了View和Model的解耦,但是这样比较麻烦,每个V都需要对应一个Presenter及View接口,实现较为复杂。
MVVM 中View和Model通过ViewModel实现数据互通,与Databinding的配合使用实现了View和Model的双向绑定,主要也是通过观察者模式来实现相互的解耦。个人觉得是目前最为常用的Android软件开发的一种模式了。当然业务非常简单且后期不需要太多的维护的就还是用MVC,写法简单。较为复杂的还是推荐MVVM模式。
2、简要说一下四种启动模式及应用场景
1)、standard:Activity默认的启动方式,每次启动都会往Activity栈中不断添加新的Activity实例。最经常使用的一种模式
2)、singleTop:栈顶复用模式,当启动一个Activity时,如果Activity栈的栈顶不是这个Activity的实例,则创建新的实例加入栈中。主要应用于自己打开自己,例如:电商APP中,商品详情页面下面会有推荐商品,点击还是商品详情页面,此时使用singleTop
3)、singleTask:栈内复用模式,当启动一个新的Activity时,如果目标Activity栈中不存在此Activity实例,则创建新的实例加入栈中。如果存在此Activity实例的话,将此实例上面的实例出栈,将自己置于栈顶并获取焦点。主要用于应用程序的主页
4)、singleInstance:当启动一个新的Activity时,都会新建一个任务栈,自己独栈一个Activity栈,比较少用。实际应用中,我好像没用过这个,应用场景:在做支付时,我感觉调用支付宝微信支付的页面应该就属于这种启动模式,不知道是不是。
ActivityA启动ActivityB的完整生命周期、启动过程(AMS怎么知道要启动哪个Activity)
ActivityA启动ActivityB时的生命周期:
A-onPause
B-onCreate
B-onStart
B-onResume
A-onStop
ActivityB返回ActivityA的生命周期:
B-onPause
A-onRestart
A-onStart
A-onResume
B-onStop
B-onDestory
Activity的启动过程:(AMS怎么知道你要启动的是哪个页面)
重新总结一下:
AMS就是通过ActivityStackSupervisor来直接或间接管理ActivityStack、TaskRecord、ActivityRecord,从ActivityRecord中就能得知要启动的Activity是哪一个,知道要启动的Activity是哪个包下的。
Instrumentation通过Binder与AMS通信,知道了要启动哪个页面,之后AMS又通过binder将启动的操作交给ActivityThread,之后执行scheduleTransaction方法,通过mH(Handler)来切换线程,最终执行handleLaunchActivity方法去启动Activity。真正启动方法是:performLaunchActivity方法,通过ActivityClientRecord获取ActivityInfo对象,实例话具体的Activity实例、ContextImpl实例等等,之后执行生命周期的一些方法。
答:ActivityRecord是存储一个Activity所有信息的类,可以代表一个Activity。AMS在创建时,初始化了一个ActivityStackSupervisor实例,执行startActivityLocked方法时初始化的ActivityRecord对象,用于存储要启动的Activity的所有信息,如:调用者所在的进程、包名信息、启动来源的Activity包名、配置信息、Intent的信息等。
3、线程A与线程B如何通信的,线程间通信原理
答:线程A要往线程B发送消息,则需要在线程B中初始化一个Handler对象,在线程B初始化Handler对象前需要确保线程B中有Looper对象(Looper.prepare(),一个线程只能有一个Looper对象,多次执行Looper.prepare()方法会抛出异常),否则在初始化Handler时会抛出异常。在线程A中,持有线程B的Handler对象实例,使用handler.sendMessage(Message msg)方法,最终还是调用sendMessageAtTime(),将Mesaage对象存入MessageQueue消息队列中,MessageQueue消息队列是Looper.prepare方法初始化的。线程B中Looper.loop方法就会开启一个无限循环,不断的从MessageQueue消息队列中取出Message对象,Message对象中有一个属性target,其实就是对应的handler,调用msg.target.dispatchMessage()方法,最终就回调到handler的handlerMessage方法了,也就是线程B的handler的handlerMessage方法去处理消息了。整个线程间通信的流程就是这样的。具体源码分析过程可以看看我的另外一篇博客:Android面试基础
4、View与ViewGroup的onMesure的方法有什么去区别
ViewGroup除了会测量自身,还会遍历所有子View进行测量。
这里再总结一下,争取下次遇到这个问题时能答的更好:
答:简单来讲就是:ViewGroup除了测量自身,还需要测量子View的大小,ViewGroup中提供了对子View的测量方法: measureChildren(int widthMeasureSpec, int heightMeasureSpec),在measureChildren中遍历所有子View,调用measureChild((int widthMeasureSpec, int heightMeasureSpec),在measureChild中调用了View的measure(int widthMeasureSpec, int heightMeasureSpec)方法,最终执行View的onMeasure()方法,让子View测量自身大小。
实际上ViewGroup是View的子类,也就说ViewGroup可以看成一个特殊的View,我们在这里可以不叫ViewGroup和View,可以说成父控件和子控件比较容易理解。
父控件在测量时,会测量自身,然后调用measureChild方法去循环测量子控件,测量子控件时,会带两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件,子控件拿着这些条件就能正确的测量自身的宽高了。也就是子控件测量自身时,需要关注父控件的两个参数。至于这个限制条件就要了解一下MeasureSpec类,MeasureSpec是View的内部类,值保存在一个int值当中,一个int有32位,前两位是 mode(模式),后30位是 size(大小) 即:MeasureSpec = mode + size。ViewGroup中有一个getChildMeasureSpec方法,这个方法规定了子View的测量模式及大小。主要就是规则就是:
- 如果父View是EXACTLY模式(match_parent):子View如果是固定了大小,则子View大小为自己的大小,测量模式为:EXACTLY;子控件是match_parent,将父View的大小赋值给子View,测量模式为:EXACTLY;如果子View是wrap_content,设置子View的最大尺寸为父View,测量模式为:AT_MOST
- 如果父View是AT_MOST模式(wrap_content):如果子view有自己的尺寸,则使用自己的尺寸,测量模式为:EXACTLY;当子View是match_parent,父View的尺寸为子View的最大尺寸,测量模式为:AT_MOST;如果子View是wrap_content,父View的尺寸为子View的最大尺寸,测量模式为:AT_MOST
- 如果父View没有做任何限制(UNSPECIFIED):如果子view有自己的尺寸,则使用自己的尺寸,测量模式为:EXACTLY;因父View没有对子View做出限制,当子View为MATCH_PARENT时则大小为0,测量模式为:UNSPECIFIED;因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0,测量模式为:UNSPECIFIED;
答:View和ViewGroup的事件分发主要区别在于ViewGroup比View多了一个拦截事件onInterceptTouchEvent方法,如果返回true,则交给自身的onTouchEvent方法,不再往下继续分发。看自身是否要消费事件,如果消费事件,则onTouchEvent返回true,不再向上传递。View就是一个View,不存在子View,所以不需要拦截,就是消费或者不消费的问题。
答:OKHttp采用建造者模式来初始化OkHttpClient以及Request对象,OkHttpClient中包含有调度器(Dispatcher)、连接池(ConnectionPool)、拦截器(Interceptor)等等,Request对象里面包含有method、url、header、body等等,通过OkHttpClient对象调用newCall方法获取Call对象(实际上是RealCall对象),之后通过call对象进行同步/异步请求。
同步请求将任务加入到调度器(Dispatcher)的runningSyncCalls双端队列中,然后直接调用了getResponseWithInterceptorChain。
异步请求将请求加入到调度器(Dispatcher)中,经历两个阶段:readyAsyncCalls、runningAsyncCalls,之后调用getResponseWithInterceptorChain。
调度器中定义了两个并发执行数的变量,分别对并发做限制,最大同时执行数为64,同主机最大执行数为5。
任务执行是通过线程池进行的,OkHttpClient的线程池是用的CacheThreadPool,其特点是核心线程为0的无界线程池,任务到达后检查是否有可复用的线程,没有则创建新的线程去执行任务,线程空闲超时时间为60秒。
getResponseWithInterceptorChain方法是通过责任链模式来执行的一串拦截器链,拦截器总共有七个,两个自定义拦截器,五个内置拦截器。其执行顺序是:
- 自定义拦截器,通过addInterceptor添加的自定义拦截器,可以用来添加header头等等操作
- 重定向拦截器,会对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作
- 桥拦截器,构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包
- 缓存拦截器,主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
- 连接拦截器,负责建立连接,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
- network拦截器,自定义网络拦截器,这个用的比较少,添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试
- 呼叫拦截器,前面的链接拦截器其实已经建立起了连接,这里就是负责读写数据,执行read、write的相关操作了
至于这些拦截器、缓存之类的具体实现逻辑我记得不是很清楚了,有需要的时候再去看看源码,之前也写过一篇博客来记录这些,博客地址:OkHttp相关源码解读
我想要打印请求的一些信息以及返回的一些信息怎么处理 ?
答:这个我们可以利用上面所说的拦截器来实现,通过OkHttp的建造者模式构建OkHttpClient时的一个addInterceptor方法添加一个自定义拦截器,实现Interceptor的intercept方法,利用Chain对象可以获取到Request信息以及Response信息。
7、你做过哪些性能优化相关的事情?
答:性能优化包括的内容比较多,例如:App的启动优化、内存优化、稳定性优化、APK瘦身优化、操作流畅度优化等等。
- APP启动优化上,之前通过阅读APP启动流程相关源码及一些博文了解到,APP启动最终会通过AMS通知ActivityThread去初始化Application、Activity,也就是能优化的点就是在Application的onCreate方法、attachBaseContext方法以及Activity的onCreate方法里面去做优化。我在项目中,如果非必须的及时初始化的SDK我就会使用阿里的Alpha框架来对第三方SDK进行初始化。同时,我记得是5.0以后,对Android方法数限制65536的问题所引入的MultiDex框架,它需要在attachBaseContext方法中进行install,那我们如果也需要加载额外的dex文件的话,非必须实时加载,也可以使用异步的方式去进行加载。至于Activity的onCreate方法中,最主要的就是setContentView方法了,加载的这个布局尽量不要有太多的嵌套,使用自定义View的方式或者ViewStub、include、merge的方式,减少层级嵌套,也就是对布局的一些优化。同时最好把系统默认的主题背景设置为空白,这样减少一次绘制背景的时间。
- 内存优化:在我开发过程中,就尽量避免一些资源使用完成后未关闭资源连接,也养成注册的一些广播在销毁的时候取消注册等一些代码习惯的问题。然后针对Handler、静态变量的使用上也要比较注意,因为这些有可能导致内存泄漏,最终导致OOM的产生,比如Handler的问题,发送的Message可能无法及时处理,生命周期比Activity生命周期长的时候就造成内存泄漏,GC无法及时回收Activity,造成Activity的内存泄漏。还有一个也非常常用的webview页面,因为webview比较耗费内存,一般就采用指定单独进程去运行。在功能完成之后,时间充裕的情况下,我就会利用LeakCanary 这款性能检测工具去检测APP里面是不是有内存泄漏的地方,然后再去做优化。同时也会使用AndroidStudio里面自带的Profiler工具,查看当前内存、网络使用情况,然后根据具体情况去分析优化。
(多说一句:如果遇到说Bitmap的内存优化的时候,就了解一下Glide、Picasso一些图片加载框架吧,Bitmap优化一般不需要自己处理了,已经有很多第三方框架帮我们处理的很好了,有轮子用就直接装上,只不过这个我们有时间的时候也要去了解一下这个轮子,要不然哪天翻车爆胎了束手无策就不行了。Glide的处理Bitmap的时候用到了BitmapPool池化的概念,以及内存缓存、磁盘缓存等等。) - 稳定性优化:这个我最主要是在Crash、ANR方面的优化,也不能算是优化吧,就是尽量减少Crash产生的概率,监控Crash产生的时间、地点(哪个模块哪个代码)、人物(什么机型),利用Thread.UncaughtExceptionHandler 接口来捕获全局异常,并记录到我们自己的服务器端,当然也会使用第三方如友盟的一些服务。利用这些信息来做一些优化。ANR方面,基本就是在开发阶段,如果产生ANR又不知道哪里出的问题的时候,可以去看看data/anr/目录下面的trace.txt文件,里面记录了一些ANR产生的信息。
- APK瘦身优化:这个我主要是对图片上、资源文件上、还有一些不必要的so包,开启混淆等操作。图片现在Android支持了SVG,有SVG的资源最好就使用SVG的资源,相对来讲比较节省空间,webp图片格式也比jpg、png等格式的图片占用空间小。
- 流畅度优化:一些页面要是层级嵌套太深我就会使用自定义组合控件的方式去做,或者使用inclue、merge、ViewStub等方式去写页面。然后现在准备再去研究一下Jetpack的 Compose框架,这个框架就类似Flutter,时一个申明式UI框架,听起来就挺厉害也挺唬人的,有时间的话,会再去看看这个最新的UI框架。除了这个UI方面的流程度,还有主线程的卡顿,Android是16ms刷新机制,不要在主线程中去做一些比较大的计算,要不然就会影响UI刷新造成卡顿的现象。
8、Bitmap的优化
答:Bitmap方面的优化就是压缩、bitmap复用等等。这里了解一点:Bitmap的内存不是由图片文件大小决定的,而是由图片长宽及单位像素所占字节数来确定的:长 * 宽 * 单位像素占用字节数。其他的就去了解一下Glide对Bitmap的优化。
9、HashMap底层原理了解过么?说说HashMap的底层原理
答:HashMap的底层是数组+链表实现的,数组长度默认为16,负载因子0.75,也就是说当数组长度超过12的时候,数组会自动扩容,扩容倍数为2。存储在数组位置是由key的hashCode值的hash散列值与数组长度的取模得来的,即hash(key.hashCode()) / 数组长度。当数组扩容后,数组存储位置要发生改变,所以数组扩容是一个比较耗费资源的一个操作。同时,数组上存储的是链表,1.7之前采用单链表,1.8之后采用单链表和红黑树,当链表长度超过8时,单链表改为红黑树,当长度变为6的时候,改回单链表,因为链表长度为6时,单链表和红黑树的查询时间复杂度一样,当链表超过6的时候,红黑树效率要比单链表高,为什么设置为8改变,应该是为了中间有一个过渡,使得不会在单链表和红黑树之间频繁的切换。
<span style="color:#172b4d"><span style="color:#333333"><span style="background-color:#2d2d2d"><span style="color:#cccccc"><code class="language-csharp"> <span style="color:#999999">/**
* 冒泡排序
* 1、通过每一次遍历获取最大值/最小值
* 2、将最大值/最小值放在尾部/头部
* 3、除开最大值/最小值,剩下的数据进行遍历获取最大/最小值
* @param arr
*/</span>
<span style="color:#cc99cd">public</span> <span style="color:#cc99cd">static</span> <span style="color:#cc99cd">void</span> <span style="color:#f08d49">bubbleSort</span>(<span style="color:#cc99cd">int</span>[] arr){
<span style="color:#cc99cd">for</span>(<span style="color:#cc99cd">int</span> i<span style="color:#67cdcc">=</span><span style="color:#f08d49">0</span>; i <span style="color:#67cdcc"><</span> arr.length; i<span style="color:#67cdcc">++</span>){
<span style="color:#cc99cd">for</span>(<span style="color:#cc99cd">int</span> j<span style="color:#67cdcc">=</span><span style="color:#f08d49">0</span>; j <span style="color:#67cdcc"><</span> arr.length <span style="color:#67cdcc">-</span>i <span style="color:#67cdcc">-</span><span style="color:#f08d49">1</span>; j<span style="color:#67cdcc">++</span>){
<span style="color:#999999">//遍历获取最大值,放在尾部</span>
<span style="color:#cc99cd">if</span>(arr[j] <span style="color:#67cdcc">></span> arr[j<span style="color:#67cdcc">+</span><span style="color:#f08d49">1</span>]){
<span style="color:#999999">//交换</span>
<span style="color:#cc99cd">int</span> temp <span style="color:#67cdcc">=</span> arr[j<span style="color:#67cdcc">+</span><span style="color:#f08d49">1</span>];
arr[j<span style="color:#67cdcc">+</span><span style="color:#f08d49">1</span>] <span style="color:#67cdcc">=</span> arr[j];
arr[j] <span style="color:#67cdcc">=</span> temp;
}
}
}
}</code></span></span></span></span>