金三银四 Android 面试的一些感受,附加面试题
接口和抽象类的本质区别
Android Jetpack 最新的组件原理
注解、反射、泛型
Handler 消息机制,生产者和消费者模型
View、ViewGroup 的事件传递机制,如何解决滑动冲突? 回答如何滑动-冲突最好是举出实际的场景和怎么解决的
View、ViewGroup 的绘制流程
okHttp、Retrofit 的源码,原理
解释一下什么是 MVP 架构
Https 原理,加密算法
RecyclerView 的缓存机制
常见的设计模式主要问到了这几个(单例、代理、适配器、建造者),先说概念,然后面试官会问具体的使用场景
最新的 Google AAC 架构(ViewModel、LiveData、Room 等等)有没有在使用,以及背后的实现原理
Kotlin 有没有在使用,问这个问题的公司,基本上自己的公司在使用 Kotlin 开发新 App,要么在使用 Kotlin 迁移、重构、与 java 混合在一起
Android 常见的内存泄漏原因,以及检查工具,主要是问如何使用 Android Profile 检查内存泄漏的,性能分析怎么做?以及第三方检查内存泄漏的工具 LeakCanary 的原理?
开发的 App 有哪些亮点,难点、如何排查线上的 bug,有没有重构代码的经验 android 的进程间的通信方式、多线程下载你是怎么做的?
android 的进程间的通信方式、多线程下载你是怎么做的
以上的面试题,主要是 Android 应用层知识,需要面试之前造造火箭的,还需要平时的耕耘、积累和总结。
三丶真题(附答案)
1.Java 中引用类型的区别,具体的使用场景
Java 中引用类型分为四类: 强引用、软引用、弱引用、虚引用。
强引用: 强引用指的是通过 new 对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。
软引用: 软引用是通过 SoftRefrence 实现的,它的生命周期比强引用短,在内存不足,抛出 OOM 之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。
弱引用: 弱引用是通过 WeakRefrence 实现的,它的生命周期比软引用还短,GC 只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。
虚引用: 虚引用是通过 FanttomRefrence 实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在 finalize 后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。
2.volatile
一般提到 volatile,就不得不提到内存模型相关的概念。我们都知道,在程序运行中,每条指令都是由 CPU 执行的,而指令的执行过程中,势必涉及到数据的读取和写入。程序运行中的数据都存放在主存中,这样会有一个问题,由于 CPU 的执行速度是要远高于主存的读写速度,所以直接从主存中读写数据会降低 CPU 的效率。为了解决这个问题,就有了高速缓存的概念,在每个 CPU 中都有高速缓存,它会事先从主存中读取数据,在 CPU 运算之后在合适的时候刷新到主存中。
这样的运行模式在单线程中是没有任何问题的,但在多线程中,会导致缓存一致性的问题。举个简单的例子:i=i+1 ,在两个线程中执行这句代码,假设 i 的初始值为 0。我们期望两个线程运行后得到 2,那么有这样的一种情况,两个线程都从主存中读取 i 到各自的高速缓存中,这时候两个线程中的 i 都为 0。在线程 1 执行完毕得到 i=1,将之刷新到主存后,线程 2 开始执行,由于线程 2 中的 i 是高速缓存中的 0,所以在执行完线程 2 之后刷新到主存的 i 仍旧是 1。
所以这就导致了对共享变量的缓存一致性的问题,那么为了解决这个问题,提出了缓存一致性协议:当 CPU 在写数据时,如果发现操作的是共享变量,它会通知其他 CPU 将它们内部的这个共享变量置为无效状态,当其他 CPU 读取缓存中的共享变量时,发现这个变量是无效的,它会从新从主存中读取最新的值。
在 Java 的多线程开发中,有三个重要概念:原子性、可见性、有序性。原子性: 一个或多个操作要么都不执行,要么都执行。可见性: 一个线程中对共享变量(类中的成员变量或静态变量)的修改,在其他线程立即可见。有序性: 程序执行的顺序按照代码的顺序执行。把一个变量声明为 volatile,其实就是保证了可见性和有序性。可见性我上面已经说过了,在多线程开发中是很有必要的。这个有序性还是得说一下,为了执行的效率,有时候会发生指令重排,这在单线程中指令重排之后的输出与我们的代码逻辑输出还是一致的。但在多线程中就可能发生问题,volatile 在一定程度上可以避免指令重排。
volatile 的原理是在生成的汇编代码中多了一个 lock 前缀指令,这个前缀指令相当于一个内存屏障,这个内存屏障有 3 个作用:
确保指令重排的时候不会把屏障后的指令排在屏障前,确保不会把屏障前的指令排在屏障后。
修改缓存中的共享变量后立即刷新到主存中。
当执行写操作时会导致其他 CPU 中的缓存无效。
3.进程间通信的方式有哪几种
AIDL 、广播、文件、socket、管道
4.Android 性能优化工具使用(这个问题建议配合 Android 中的性能优化)
Android 中常用的性能优化工具包括这些:Android Studio 自带的 Android Profiler、LeakCanary、BlockCanary
Android 自带的 Android Profiler 其实就很好用,Android Profiler 可以检测三个方面的性能问题:CPU、MEMORY、NETWORK。
LeakCanary 是一个第三方的检测内存泄漏的库,我们的项目集成之后 LeakCanary 会自动检测应用运行期间的内存泄漏,并将之输出给我们。
BlockCanary 也是一个第三方检测 UI 卡顿的库,项目集成后 Block 也会自动检测应用运行期间的 UI 卡顿,并将之输出给我们。
5.Android 中的类加载器
PathClassLoader,只能加载系统中已经安装过的 apkDexClassLoader,可以加载 jar/apk/dex,可以从 SD 卡中加载未安装的 apk
6.Android 中的动画有哪几类,它们的特点和区别是什么
Android 中动画大致分为 3 类: 帧动画、补间动画(View Animation)、属性动画(Object Animation)。
帧动画: 通过 xml 配置一组图片,动态播放。很少会使用。
补间动画(View Animation): 大致分为旋转、透明、缩放、位移四类操作。很少会使用。
属性动画(Object Animation
): 属性动画是现在使用的最多的一种动画,它比补间动画更加强大。属性动画大致分为两种使用类型,分别是ViewPropertyAnimator
和ObjectAnimator
。前者适合一些通用的动画,比如旋转、位移、缩放和透明,使用方式也很简单通过View.animate()
即可得到ViewPropertyAnimator
,之后进行相应的动画操作即可。后者适合用于为我们的自定义控件添加动画,当然首先我们应该在自定义 View 中添加相应的getXXX()
和setXXX()
相应属性的getter
和setter
方法,这里需要注意的是在 setter 方法内改变了自定义 View 中的属性后要调用invalidate()
来刷新 View 的绘制。之后调用ObjectAnimator.of
属性类型()返回一个ObjectAnimator
,调用start()
方法启动动画即可。
补间动画与属性动画的区别:
补间动画是父容器不断的绘制 view,看起来像移动了效果,其实 view 没有变化,还在原地。
是通过不断改变 view 内部的属性值,真正的改变 view。
7.Handler 机制
说到 Handler,就不得不提与之密切相关的这几个类:Message
、MessageQueue
,Looper
。
Message。 Message 中有两个成员变量值得关注:
target
和callback
。target 其实就是发送消息的 Handler 对象,callback 是当调用handler.post(runnable)
时传入的Runnable
类型的任务。post 事件的本质也是创建了一个Message
,将我们传入的这个 runnable 赋值给创建的Message
的callback
这个成员变量。MessageQueue。消息队列很明显是存放消息的队列,值得关注的是
MessageQueue
中的next()
方法,它会返回下一个待处理的消息。Looper。Looper 消息轮询器其实是连接 Handler 和消息队列的核心。首先我们都知道,如果想要在一个线程中创建一个 Handler,首先要通过
Looper.prepare()
创建 Looper,之后还得调用Looper.loop()
开启轮询。我们着重看一下这两个方法。
prepare()
。这个方法做了两件事:首先通过ThreadLocal.get()
获取当前线程中的 Looper,如果不为空,则会抛出一个 RunTimeException,意思是一个线程不能创建 2 个 Looper。如果为 null 则执行下一步。第二步是创建了一个 Looper,并通过ThreadLocal.set(looper)
。将我们创建的 Looper 与当前线程绑定。这里需要提一下的是消息队列的创建其实就发生在 Looper 的构造方法中。
loop()
。这个方法开启了整个事件机制的轮询。它的本质是开启了一个死循环,不断的通过MessageQueue
的next()
方法获取消息。拿到消息后会调用msg.target.dispatchMessage()
来做处理。其实我们在说到 Message 的时候提到过,msg.target 其实就是发送这个消息的 handler。这句代码的本质就是调用 handler 的dispatchMessage()
。Handler。上面做了这么多铺垫,终于到了最重要的部分。Handler 的分析着重在两个部分:发送消息和处理消息
Handler。上面做了这么多铺垫,终于到了最重要的部分。Handler 的分析着重在两个部分:发送消息和处理消息
发送消息。其实发送消息除了sendMessage
之外还有sendMessageDelayed
和 post 以及 postDelayed 等等不同的方式。但它们的本质都是调用了sendMessageAtTime
。在sendMessageAtTime
这个方法中调用了enqueueMessage
。在enqueueMessage
这个方法中做了两件事:通过msg.target = this
实现了消息与当前 handler 的绑定。然后通过queue.enqueueMessage
实现了消息入队。
处理消息。消息处理的核心其实就是dispatchMessage()
这个方法。这个方法里面的逻辑很简单,先判断msg.callback
是否为 null,如果不为空则执行这个runnable
。如果为空则会执行我们的handleMessage
方法。
8.Android 性能优化
Android 中的性能优化在我看来分为以下几个方面:内存优化、布局优化、网络优化、安装包优化。
内存优化: 下一个问题就是。
布局优化: 布局优化的本质就是减少 View 的层级。常见的布局优化方案如下
在 LinearLayout 和 RelativeLayout 都可以完成布局的情况下优先选择 RelativeLayout,可以减少 View 的层级
将常用的布局组件抽取出来使用 < include > 标签
通过 < ViewStub > 标签来加载不常用的布局
使用 < Merge > 标签来减少布局的嵌套层次
网络优化: 常见的网络优化方案如下
尽量减少网络请求,能够合并的就尽量合并
避免 DNS 解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在 DNS 劫持的风险。可以根据业务需求采用增加动态更新 IP 的方式,或者在 IP 方式访问失败时切换到域名访问方式。
大量数据的加载采用分页的方式
网络数据传输采用 GZIP 压缩
加入网络数据的缓存,避免频繁请求网络
上传图片时,在必要的时候压缩图片
安装包优化: 安装包优化的核心就是减少 apk 的体积,常见的方案如下
使用混淆,可以在一定程度上减少 apk 体积,但实际效果微乎其微
减少应用中不必要的资源文件,比如图片,在不影响 APP 效果的情况下尽量压缩图片,有一定的效果
在使用了 SO 库的时候优先保留 v7 版本的 SO 库,删掉其他版本的 SO 库。原因是在 2018 年,v7 版本的 SO 库可以满足市面上绝大多数的要求,可能八九年前的手机满足不了,但我们也没必要去适配老掉牙的手机。实际开发中减少 apk 体积的效果是十分显著的,如果你使用了很多 SO 库,比方说一个版本的 SO 库一共 10M,那么只保留 v7 版本,删掉 armeabi 和 v8 版本的 SO 库,一共可以减少 20M 的体积。
9.Android 内存优化
Android 的内存优化在我看来分为两点: 避免内存泄漏、扩大内存,其实就是开源节流。
其实内存泄漏的本质就是较长生命周期的对象引用了较短生命周期的对象。
常见的内存泄漏:
单例模式导致的内存泄漏。 最常见的例子就是创建这个单例对象需要传入一个 Context,这时候传入了一个 Activity 类型的 Context,由于单例对象的静态属性,导致它的生命周期是从单例类加载到应用程序结束为止,所以即使已经 finish 掉了传入的 Activity,由于我们的单例对象依然持有 Activity 的引用,所以导致了内存泄漏。解决办法也很简单,不要使用 Activity 类型的 Context,使用 Application 类型的 Context 可以避免内存泄漏。
静态变量导致的内存泄漏。 静态变量是放在方法区中的,它的生命周期是从类加载到程序结束,可以看到静态变量生命周期是非常久的。最常见的因静态变量导致内存泄漏的例子是我们在 Activity 中创建了一个静态变量,而这个静态变量的创建需要传入 Activity 的引用 this。在这种情况下即使 Activity 调用了 finish 也会导致内存泄漏。原因就是因为这个静态变量的生命周期几乎和整个应用程序的生命周期一致,它一直持有 Activity 的引用,从而导致了内存泄漏。
非静态内部类导致的内存泄漏。 非静态内部类导致内存泄漏的原因是非静态内部类持有外部类的引用,最常见的例子就是在 Activity 中使用 Handler 和 Thread 了。使用非静态内部类创建的 Handler 和 Thread 在执行延时操作的时候会一直持有当前 Activity 的引用,如果在执行延时操作的时候就结束 Activity,这样就会导致内存泄漏。解决办法有两种:第一种是使用静态内部类,在静态内部类中使用弱引用调用 Activity。第二种方法是在 Activity 的 onDestroy 中调用 handler.removeCallbacksAndMessages 来取消延时事件。
使用资源未及时关闭导致的内存泄漏。 常见的例子有:操作各种数据流未及时关闭,操作 Bitmap 未及时 recycle 等等。
**使用第三方库未能及时解绑。**有的三方库提供了注册和解绑的功能,最常见的就是 EventBus 了,我们都知道使用 EventBus 要在 onCreate 中注册,在 onDestroy 中解绑。如果没有解绑的话,EventBus 其实是一个单例模式,他会一直持有 Activity 的引用,导致内存泄漏。同样常见的还有 RxJava,在使用 Timer 操作符做了一些延时操作后也要注意在 onDestroy 方法中调用 disposable.dispose()来取消操作。
属性动画导致的内存泄漏。 常见的例子就是在属性动画执行的过程中退出了 Activity,这时 View 对象依然持有 Activity 的引用从而导致了内存泄漏。解决办法就是在 onDestroy 中调用动画的 cancel 方法取消属性动画。
WebView 导致的内存泄漏。 WebView 比较特殊,即使是调用了它的 destroy 方法,依然会导致内存泄漏。其实避免 WebView 导致内存泄漏的最好方法就是让 WebView 所在的 Activity 处于另一个进程中,当这个 Activity 结束时杀死当前 WebView 所处的进程即可,我记得阿里钉钉的 WebView 就是另外开启的一个进程,应该也是采用这种方法避免内存泄漏。
扩大内存,为什么要扩大我们的内存呢?有时候我们实际开发中不可避免的要使用很多第三方商业的 SDK,这些 SDK 其实有好有坏,大厂的 SDK 可能内存泄漏会少一些,但一些小厂的 SDK 质量也就不太靠谱一些。那应对这种我们无法改变的情况,最好的办法就是扩大内存。
扩大内存通常有两种方法:一个是在清单文件中的 Application 下添加 largeHeap=”true”这个属性,另一个就是同一个应用开启多个进程来扩大一个应用的总内存空间。第二种方法其实就很常见了,比方说我使用过个推的 SDK,个推的 Service 其实就是处在另外一个单独的进程中。
Android 中的内存优化总的来说就是开源和节流,开源就是扩大内存,节流就是避免内存泄漏。
10.Binder 机制
在 Linux 中,为了避免一个进程对其他进程的干扰,进程之间是相互独立的。在一个进程中其实还分为用户空间和内核空间。这里的隔离分为两个部分,进程间的隔离和进程内的隔离。
既然进程间存在隔离,那其实也是存在着交互。进程间通信就是 IPC,用户空间和内核空间的通信就是系统调用。
Linux 为了保证独立性和安全性,进程之间不能直接相互访问,Android 是基于 Linux 的,所以也是需要解决进程间通信的问题。
其实 Linux 进程间通信有很多方式,比如管道、socket 等等。为什么 Android 进程间通信采用了 Binder 而不是 Linux 已有的方式,主要是有这么两点考虑:性能和安全
性能。在移动设备上对性能要求是比较严苛的。Linux 传统的进程间通信比如管道、socket 等等进程间通信是需要复制两次数据,而 Binder 则只需要一次。所以 Binder 在性能上是优于传统进程通信的。
安全。传统的 Linux 进程通信是不包含通信双方的身份验证的,这样会导致一些安全性问题。而 Binder 机制自带身份验证,从而有效的提高了安全性。
Binder 是基于 CS 架构的,有四个主要组成部分。
Client。客户端进程。
Server。服务端进程。
ServiceManager。提供注册、查询和返回代理服务对象的功能。
Binder 驱动。主要负责建立进程间的 Binder 连接,进程间的数据交互等等底层操作。
Binder 机制主要的流程是这样的:
服务端通过 Binder 驱动在 ServiceManager 中注册我们的服务。
客户端通过 Binder 驱动查询在 ServiceManager 中注册的服务。
ServiceManager 通过 Binder 驱动返回服务端的代理对象。
评论