1、Android MVVM 架构。
MVC: Activity 充当了 View 和 Controller ,基本都是直接在 Activity 页面处理数据,业务等等,单个页面代码过多不好维护,难以进行单元测试,不适用于打造稳定的 Android 项目。
MVP:
解决了 MVC 中 Activity 充当了 View 和 Controller 的问题,其核心理念是通过一个抽象的 View 接口(不是真正的 View 层)将 Presenter 与真正的 View 层进行解耦。Persenter 持有该 View 接口,对该接口进行操作,而不是直接操作 View 层。这样就可以把视图操作和业务逻辑解耦,从而让 Activity 成为真正的 View 层。也是现今比较流行的架构。
弊端在于如果业务复杂了,也可能导致 P 层太臃肿,而且 V 层和 P 层有一定耦合度,如果 V 层需要更改,那么 P 层不只改一个地方那么简单,还需要改 View 的接口及其实现,牵一发动全身。
MVVM: MVVM 的目标和思想与 MVP 类似,利用数据绑定(Data Binding)、依赖属性(Dependency Property)、命令(Command)、路由事件(Routed Event)等新特性,打造了一个低耦合度更加灵活高效的架构。
MVVM 模式中,数据是独立于 UI 的。数据和业务逻辑处于一个独立的 ViewModel 中,ViewModel 只需要关注数据和业务逻辑,不需要和 UI 或者控件打交道。UI 想怎么处理数据都由 UI 自己决定,ViewModel 不涉及任何和 UI 相关的事,也不持有 UI 控件的引用。它非常完美的跟 View 层解耦了,解决了 MVP 的痛点。Model 只负责提供数据,ViewModel 只负责处理数据,View 则只负责显示数据和处理与用户的交互。
参考资料:《使用Kotlin构建更适合Android的MVVM应用程序》
2、kotlin 的空安全设计。
简单来说就是通过 IDE 的提示来避免调用 null 对象,从而避免 NullPointerException。
Kotlin 有语言级别的默认支持,而且提示的级别从 warning 变成了 error(拒绝编译)。
在 Kotlin 里面,所有的变量默认都是不允许为空的,如果你给它赋值 null,编译就会报错。
对于那些可以为空值的变量,你可以在类型右边加一个 ? 号,解除它的非空限制,这种类型之后加 ? 的写法,在 Kotlin 里叫可空类型。
「可能为空」的变量在 Kotlin 里使用时用的不是 . 而是 ?.:
🏝️var view: View? = nullview?.setBackgroundColor(Color.RED)这个写法会对变量做一次非空确认之后再调用方法,这是 Kotlin 的写法,并且它可以做到线程安全,因此这种写法叫做「safe call」。
另外还有一种双感叹号的用法:
🏝️view!!.setBackgroundColor(Color.RED)意思是告诉编译器,我保证这里的 view 一定是非空的,编译器你不要帮我做检查了,有什么后果我自己承担。这种「肯定不会为空」的断言式的调用叫做 「non-null asserted call」。
以上就是 Kotlin 的空安全设计。
参考资料:《Kotlin 的变量、函数和类型》
3、Kotlin 的伴生对象。
Java 静态变量和方法的等价写法:companion object 变量和函数。
静态初始化:Java 中的静态变量和方法,在 Kotlin 中都放在了 companion object 中。因此 Java 中的静态初始化在 Kotlin 中自然也是放在 companion object 中的,像类的初始化代码一样,由 init 和一对大括号表示:
🏝️class Sample { 👇 companion object { 👇 init { ... } }}ps:companion 可以理解为伴随、伴生,表示修饰的对象和外部类绑定。
一个类中最多只可以有一个伴生对象,但可以有多个嵌套对象。这样的好处是调用的时候可以省掉伴生对象名。
参考资料:《Kotlin 里那些「不是那么写的」》
4、Kotlin 的协程。
协程就是一套由 Kotlin 官方提供的线程 API。
一般在需要切线程或者指定线程的时候使用。
类似 Java 的 Executor 和 Android 的 AsyncTask。
协程最基本的功能是并发,也就是多线程。最大的好处在于可以把运行在不同线程的代码写在同一个代码块里。
🏝️val coroutineScope = CoroutineScope(context)coroutineScope.launch(Dispatchers.Main) { // 👈 在 UI 线程开始 val image = withContext(Dispatchers.IO) { // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程 getImage(imageId) // 👈 将会运行在 IO 线程 } avatarIv.setImageBitmap(image) // 👈 回到 UI 线程更新 UI} launch 函数具体的含义是:我要创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓「协程」是谁?就是你传给 launch 的那些代码,这一段连续代码叫做一个「协程」。
withContext 函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。消除了并发代码在协作时的嵌套。
我们甚至可以把 withContext 放进一个单独的函数里面,但是需要在前面加一个 suspend 修饰符,不然编译器是会报错的。
🏝️suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) { ...}如果遇到的场景是多个网络请求需要等待所有请求结束之后再对 UI 进行更新时,可以使用 async 函数:
🏝️coroutineScope.launch(Dispatchers.Main) { // 👇 val avatar = async { api.getAvatar(user) } // 获取用户头像 val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo val merged = suspendingMerge(avatar, logo) // 合并结果 // 👆 show(merged) // 更新 UI}参考资料:《Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的》
5、 Android 多线程的实现方式。
多线程的两种实现方法分别是继承 Thread 类与实现 Runnable 接口。
推荐使用后者,因为实现 Runnable 接口相比继承 Thread 类有如下优势:
- 可以避免由于 Java 的单继承特性而带来的局限;
- 增强程序的健壮性,代码能够被多个程序共享,代码与数据是独立的;
- 适合多个相同程序代码的线程区处理同一资源的情况。
让线程同步方法有:锁 ,synchronized 块等。
ps:Java 5以后创建线程还有第三种方式:实现 Callable 接口。
6、在单线程模型中 Message、Handler、MessageQueue、Looper之间的关系。
简单的说,Handler 获取当前线程中的 Looper 对象,Looper 用来从存放 Message 的 MessageQueue 中取出 Message ,再由 Handler 进行 Message 的分发和处理。
7、 Android 内存溢出和内存泄露。
内存溢出( Out Of Memory ):系统会给每个 APP 分配内存也就是 Heap size 值,当 APP 所需要的内存大于系统分配的内存,就会造成内存溢出。
内存泄漏( Memory Leak ):一个不再被程序使用的对象或变量一直被占据在内存中,无法被回收,就会造成内存泄漏。
内存泄露是造成内存溢出的主要原因。
造成内存泄露的可能原因:
- 长生命周期的对象持有短生命周期对象的引用;
- 数据库的
cursor没有关闭; - 构造
adapter时,没有使用缓存contentview; Bitmap对象不使用时没有释放。
8、 Android OOM 的排查方法。
Android Studio 的 Android Monitor 工具可以自动进行内存泄漏检查。
9、 RxJava是什么?
一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库。
关键词:异步、逻辑简洁
亮点:随着程序逻辑变得越来越复杂,它依然能够保持简洁。
参考资料:《给 Android 开发者的 RxJava 详解》
10、 Retrofit 与 Okhttp 之间的关系。
Retrofit 是基于 Okhttp 的,它在 Okhttp 的基础上做了一层封装,使用注解方式让我们使用简单方便且看代码一目了然。Retrofit 是基于 APP 发起请求的封装,也就是面向的是应用层(比如响应数据的处理和错误处理等)。而 Okhttp 是对底层网络请求的封装与优化( Socket 优化,数据压缩,buffer 缓存等)。
参考资料:《 Okhttp 与 Retrofit 的简单介绍及两者间的联系》
11、 Activity 的启动模式。
standard 模式(默认):标准模式,每次启动 Activity 都会创建一个新的 Activity 实例,并且将其压入任务栈栈顶,而不管这个 Activity 是否已经存在。Activity的启动三回调( onCreate() -> onStart() -> onResume() )都会执行。
singleTop 模式:栈顶复用模式,这种模式下,如果新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,所以它的启动三回调就不会执行,同时 Activity 的onNewIntent()方法会被回调,如果 Activity 已经存在但是不在栈顶,那么作用与 standard 模式一样。
singleTask 模式:栈内复用模式,创建这样的 Activity 的时候,系统会先确认它所需任务栈已经创建,否则先创建任务栈,然后放入 Activity ,如果栈中已经有一个 Activity 实例,那么这个 Activity 就会被调到栈顶,onNewIntent()并且 singleTask 会清理在当前 Activity 上面的所有 Activity 。
singleInstance 模式:加强版的 singleTask 模式,这种模式的 Activity 只能单独位于一个任务栈内,由于栈内复用的特性,后续请求均不会创建新的 Activity ,除非这个独特的任务栈被系统销毁了。
参考资料:《 Android 基础知识》
12、请您描述一下 Activity A 跳转至 Activity B 时,Activity A 生命周期的变化。
( A ) onPause -> ( B ) onCreate -> ( B ) onStart -> ( B ) onResume -> ( A ) onStop
Activity B 按 back 键返回时的生命周期变化:
( B ) onPause -> ( A ) onRestart -> ( A ) onStart -> ( A ) onResume -> ( B ) onStop -> ( B ) onDestory
13、如何提高 Android APP 的启动速度?
- 用提前展示出来的 Window ,快速展示出来一个界面,给用户快速反馈的体验。
- 避免在启动时做密集沉重的初始化( Heavy app initialization )。
- 定位问题:避免 I/O 操作、反序列化、网络操作、布局嵌套等。
参考资料:《 Android :手把手教你如何优雅的实现APP启动速度优化》
14、Android 怎么自定义 View ?
实现方式:
- 自定义组合控件:多个控件组合成为一个新的控件,方便多处复用。
- 继承系统
View控件:继承TextView等系统控件,在系统控件的基础功能上进行扩展。 - 继承
View:不复用系统控件逻辑,继承View进行功能定义。 - 继承系统
ViewGroup:继承LinearLayout等系统控件,在系统控件的基础功能上进行扩展。 - 继承
ViewGroup:不复用系统控件逻辑,继承ViewGroup进行功能定义。
View 绘制流程:
measure():测量View的宽高。layout():计算当前View以及子View的位置。draw():视图的绘制工作。
参考资料:《 Android 自定义 View 全解 》
15、 Android 的事件分发机制。
最重要的四个触摸事件:DOWN , MOVE , UP 和 CANCEL 。
最重要的三个方法:dispatchTouchEvent() 、 onInterceptTouchEvent() 、 onTouchEvent() 。
onTouchEvent()
return false 表示我不消费该事件,则该事件继续向下传递。
return true 表示我消费该事件,则该事件不再继续向下传递。
onInterceptTouchEvent()
return false 表示它不想拦截,但后续的事件依然会传递给它们的 onInterceptTouchEvent() 方法,这一点与 onTouchEvent() 的行为是不一样的。
return true 拦截并接管该事件后续的处理,需要注意的是,该方法一旦 return true ,就再也不会被调用了。
ps:
它只存在于 ViewGroup 中,普通的 View 中没有这个方法。
在任何一个 View 的 onTouchEvent 被调用之前,它的父辈们( ancestors )将先获得拦截这个事件的一次机会,换句话说,它们可以窃取该事件。 ( requestDisallowInterceptTouchEvent可以阻止他们窃取该事件。)
参考资料:《可能是讲解 Android 事件分发最好的文章 》
16、Android有哪几类动画?
- Tween Animation :即补间动画,它提供了淡入淡出(
alpha)、缩放(scale)、旋转(rotate)、移动(translate)等效果。 - Frame Animation :即帧动画,它会按顺序展示一组图片(如gif、电影之类的效果)。
- Property Animation :即属性动画,在 Android 3.0之后提供,Property Animation 是一个非常强大的动画框架,它允许你对对象的任何属性进行动画操作。
ps:针对于一些复杂的动画效果,推荐用 Lottie 库,因为:
- 帧动画:需要添加大量图片(尺寸适配),势必会导致 APK 体积暴涨;
- Gif :Gif 图占用空间较大,且需适配多种屏幕,影响同上;
- 属性动画 + 图片 + SVG :繁琐且不易维护,稍作修改可能就要推倒重来。
用 Lottie 库可以让我们免于纠结复杂的动画效果 。
- 设计师通过 AE( After Effects )和 Bodymovin 插件 将动画导出 JSON 文件;
- 我们把 JSON 文件丢到
app / src / main / assets目录下 build.gradle导入lottie-android库,XML 中引入LottieAnimationView直接使用。
LottieAnimationView 直接继承自 AppCompatImageView ,LottieAnimationView 其实内部使用的是 LottieDrawable 。
参考资料:
《Android动画学习(一)——Android动画系统框架简介》
17、Android进程间怎么通信?
AIDL( Android Interface Definition Language )是一种 IDL 语言,用于生成可以在 Android 设备上两个进程之间通信( IPC )的代码。
如果在一个进程中(例如 Activity )要调用另一个进程中(例如 Service )对象的操作,就可以使用 AIDL 生成可序列化的参数。
AIDL 支持的数据类型:
- 不需要
import声明的简单 Java 编程语言类型(int,boolean等)。 String,CharSequence不需要特殊声明。List,Map和Parcelables类型,这些类型内所包含的数据成员也只能是简单数据类型。
18、 Dalvik 和标准 Java 虚拟机之间的主要差别。
- Dalvik 基于寄存器,而 JVM 基于栈。
- Dalvik 没有 JIT 编译器。
- Dalvik 常量池被修改为只使用32位的索引,以简化解析器。
- Dalvik 使用自己的字节码,而非 Java 字节码。
- Dalvik 允许在有限的内存中同时运行多个虚拟机的实例,并且每一个 Dalvik 应用作为一个独立的 Linux 进程执行。
19、Java中的强引用、软应用、弱引用和虚引用。
强引用:
最普遍的引用,比如下面这段代码中的 object 和 str :
Object object = new Object(); String str = "StrongReference";如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 会抛出 OutOfMemoryError异常,只有当这个对象没有被引用时,才有可能会被回收。
软应用:
用来描述一些有用但并不是必需的对象,在 Java 中用 java.lang.ref.SoftReference 类来表示。
对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。
因此,这一点可以很好地用来解决 OOM 的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
弱引用:
弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在 java 中,用 java.lang.ref.WeakReference 类来表示。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在 JVM 进行垃圾回收时总会被回收。
虚引用:
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在 Java 中用 java.lang.ref.PhantomReference 类表示。
如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用必须和引用队列关联使用。
参考资料:《 Java :强引用,软引用,弱引用和虚引用》
20、 Java 面向对象的六大原则。
- 单一职责原则( Single - Resposibility Principle )
- 开放封闭原则( Open - Closed principle )
- Liskov替换原则( Liskov - Substituion Principle )
- 依赖倒置原则( Dependecy - Inversion Principle )
- 接口隔离原则( Interface - Segregation Principle )
- 良性依赖原则
参考资料:《 Java 基础:面向对象六大原则》
21、 Java 的 JNI 。
JNI ( Java Native Interface ) 即 Java 本地接口,用来实现 Java 代码与本地的 C / C++ 代码进行交互。
实现方法:
- 在 Java 中先声明一个
native方法 - 编译 Java 源文件 javac 得到
.class文件 - 通过
javah -jni命令导出 JNI 的.h头文件 - 使用 Java 需要交互的本地代码,实现在 Java 中声明的 Native 方法(如果 Java 需要与 C++ 交互,那么就用 C++ 实现 Java 的 Native 方法。)
- 将本地代码编译成动态库( Windows 系统下是
.dll文件,Linux 系统下是.so文件,Mac 系统下是.jnilib) - 通过 Java 命令执行 Java 程序,最终实现 Java 调用本地代码。
ps :javah 是 JDK 自带的一个命令,-jni 参数表示将 class 中用到 native 声明的函数生成JNI 规则的函数。
参考资料:《Android JNI(一)——NDK与JNI基础》
如文中有错误,还忘指正,感谢~^^。