谈谈 MVC、MVP 和 MVVM,好在哪里,不好在哪里?
- MVC
- 视图层(View) :对应于 xml 布局文件和 java 代码动态 view 部分
- 控制层(Controller): MVC 中 Android 的控制层是由 Activity 来承担的,Activity 本来主要是作为初始化页面,展示数据的操作,但是因为 XML 视图功能太弱,所以 Activity 既要负责视图的显示又要加入控制逻辑,承担的功能过多。
- 模型层(Model) :针对业务模型,建立数据结构和相关的类,它主要负责网络请求,数据库处理,I/O 的操作。
总结:
具有一定的分层,model 彻底解耦,controller 和 view 并没有解耦层与层之间的交互尽量使用回调或者去使用消息机制去完成,尽量避免直接持有 controller 和 view 在 android 中无法做到彻底分离,但在代码逻辑层面一定要分清业务逻辑被放置在 model 层,能够更好的复用和修改增加业务。
-
MVP
通过引入接口 BaseView,让相应的视图组件如 Activity,Fragment 去实现 BaseView,实现了视图层的独立,通过中间层 Preseter 实现了 Model 和 View 的完全解耦。MVP 彻底解决了 MVC 中 View 和 Controller 傻傻分不清楚的问题,但是随着业务逻辑的增加,一个页面可能会非常复杂,UI 的改变是非常多,会有非常多的 case,这样就会造成 View 的接口会很庞大。
封装 p 层之后.如果 p 层数据过大,如何解决?
对于 MVP 模式来说,P 层如果数据逻辑过于臃肿,建议引入 RxJava 或则 Dagger,越是复杂的逻辑,越能体现 RxJava 的优越性
-
MVVM
MVP 中我们说过随着业务逻辑的增加,UI 的改变多的情况下,会有非常多的跟 UI 相关的 case,这样就会造成 View 的接口会很庞大。而 MVVM 就解决了这个问题,通过双向绑定的机制,实现数据和 UI 内容,只要想改其中一方,另一方都能够及时更新的一种设计理念,这样就省去了很多在 View 层中写很多 case 的情况,只需要改变数据就行。
三者如何选择?
- 如果项目简单,没什么复杂性,未来改动也不大的话,那就不要用设计模式或者架构方法,只需要将每个模块封装好,方便调用即可,不要为了使用设计模式或架构方法而使用。
- 对于偏向展示型的 app,绝大多数业务逻辑都在后端,app 主要功能就是展示数据,交互等,建议使用 mvvm。
- 对于工具类或者需要写很多业务逻辑 app,使用 mvp 或者 mvvm 都可。
是否能从 Android 中举几个例子说说用到了什么设计模式 ?
- AlertDialog、Notification 源码中使用了 Builder(建造者)模式完成参数的初始化
- Okhttp 内部使用了责任链模式来完成每个 Interceptor 拦截器的调用
- RxJava 的观察者模式;单例模式;GridView 的适配器模式;Intent 的原型模式
- 日常开发的 BaseActivity 抽象工厂模式
实现单例模式有几种方法 ?懒汉式中双层锁的目的是什么 ?两次判空的目的又是什么 ?
- 单例模式实现方法有多种:饿汉,懒汉(线程安全,线程非安全),双重检查(DCL),内部类,以及枚举
- 所谓双层检验锁(在加锁前后对实例对象进行两次判空的检验):加锁是为了第一次对象实例化的线程同步,而锁内还要有第二层判空是因为可能会有多个线程进入第一层 if 判断内部,而在加锁代码块外排队等候,如果锁内不进行第二次检验,仍然会出现实例化多个对象的情况。
描述一下 View 事件传递分发机制?
- View 事件分发本质就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 发生后,系统将这个点击事件传递到一个具体的 View 上。
- 点击事件的传递顺序:Activity(Window)→ViewGroup→ View。
- 事件分发过程由三个方法共同完成:
-
dispatchTouchEvent
:用来进行事件的分发。如果事件能够传递给当前 View,那么此方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件 -
onInterceptTouchEvent
:在上述方法内部调用,对事件进行拦截。该方法只在 ViewGroup 中有,View(不包含 ViewGroup)是没有的。一旦拦截,则执行ViewGroup 的 onTouchEvent,在 ViewGroup 中处理事件,而不接着分发给View。且只调用一次,返回结果表示是否拦截当前事件. -
onTouchEvent
: 在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件。
如何解决 View 的事件冲突 ? 举个开发中遇到的例子 ?
- 常见开发中事件冲突的有 ScrollView 与 RecyclerView 的滑动冲突、RecyclerView 内嵌同时滑动同一方向。
- 滑动冲突的处理规则:
- 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
- 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部 View 拦截事件,何时由内部 View 拦截事件。
- 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
- 滑动冲突的实现方法:
- 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截。
- 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合 requestDisallowInterceptTouchEvent 方法。
Handler、Thread 和 HandlerThread 的差别?
- Handler:在 android 中负责发送和处理消息,通过它可以实现其他支线线程与主线程之间的消息通讯。
- Thread:Java 进程中执行运算的最小单位,亦即执行处理机调度的基本单位。某一进程中一路单独运行的程序。
- HandlerThread:一个继承自 Thread 的类 HandlerThread,Android 中没有对 Java 中的 Thread 进行任何封装,而是提供了一个继承自 Thread 的类 HandlerThread 类,这个类对 Java 的 Thread 做了很多便利的封装。HandlerThread 继承于Thread,所以它本质就是个 Thread。与普通 Thread 的差别就在于,它在内部直接实现了 Looper 的实现,这是 Handler 消息机制必不可少的。有了自己的 looper,可以让我们在自己的线程中分发和处理消息。如果不用 HandlerThread 的话,需要手动去调用 Looper.prepare()和 Looper.loop()这些方法。
什么是 ANR ? 什么情况会出现 ANR ?如何避免 ? 在不看代码的情况下如何快速定位出现 ANR 问题所在 ?
- ANR(Application Not Responding,应用无响应):当操作在一段时间内系统无法处理时,会在系统层面会弹出 ANR 对话框。
- 产生 ANR 可能是因为 5s 内无响应用户输入事件、10s 内未结束BroadcastReceiver、20s 内未结束 Service。
- 想要避免 ANR 就不要在主线程做耗时操作,而是通过开子线程,方法比如继承 Thread 或实现 Runnable 接口、使用 AsyncTask、IntentService、HandlerThread 等。
WebView 的性能优化 ?
一个加载网页的过程中,native、网络、后端处理、CPU 都会参与,各自都有必要的工作和依赖关系;让他们相互并行处理而不是相互阻塞才可以让网页加载更快:
- WebView 初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
- 常用 JS 本地化及延迟加载,使用第三方浏览内核
- 后端处理慢,可以让服务器分 trunk 输出,在后端计算的同时前端也加载网络静态资源。
- 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
- 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
- WebView 初始化慢,就随时初始化好一个 WebView 待用。
-
DNS 和链接慢,想办法复用客户端使用的域名和链接。
内存泄露和内存溢出的区别 ?AS 有什么工具可以检测内存泄露
- 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory;比如申请了一个 integer,但给它存了 long 才能存下的数,那就是内存溢出。
- 内存泄露(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak 会最终会导致 out of memory!
- 查找内存泄漏可以使用 Android Studio 自带的 AndroidProfiler 工具或 MAT
性能优化,怎么保证应用启动不卡顿?
- 应用启动速度,取决于你在 application 里面时候做了什么事情,比如你集成了很多 sdk,并且 sdk 的 init 操作都需要在主线程里实现所以会有卡顿的感觉。在非必要的情况下可以把加载延后或则开启子线程处理。
- 另外,影响界面卡顿的两大因素,分别是界面绘制和数据处理。
- 布局优化(使用 include,merge 标签,复杂布局推荐使用 ConstraintLayout 等)
- onCreate() 中不执行耗时操作 把页面显示的 View 细分一下,放在 AsyncTask 里逐步显示,用 Handler 更好。这样用户的看到的就是有层次有步骤的一个个的
View 的展示,不会是先看到一个黑屏,然后一下显示所有 View。最好做成动画,效果更自然。 - 利用多线程的目的就是尽可能的减少 onCreate() 和 onReume() 的时间,使得用户能尽快看到页面,操作页面。
- 减少主线程阻塞时间。
- 提高 Adapter 和 AdapterView 的效率。
黑白屏怎么处理?
黑白屏产生原因:当我们在启动一个应用时,系统会去检查是否已经存在这样一个进程,如果不存在,系统的服务会先检查 startActivity 中的 intent 的信息,然后在去创建进程,最后启动 Acitivy,即冷启动。而启动出现白黑屏的问题,就是在这段时间内产生的。系统在绘制页面加载布局之前,首先会初始化窗口(Window),而在进行这一步操作时,系统会根据我们设置的 Theme 来指定它的 Theme 主题颜色,我们在 Style 中的设置就决定了显示的是白屏还是黑屏。
- windowIsTranslucent 和 windowNoTitle,将这两个属性都设置成 true (会有明显的卡顿体验,不推荐)
- 如果启动页只是是一张图片,那么为启动页专一设置一个新的主题,设置主题的 android:windowBackground 属性为启动页背景图即可
- 使用 layer-list 制作一张图片 launcher_layer.xml,将其设置为启动页专一主题的背景,并将其设置为启动页布局的背景。
说下 Activity 生命周期 ?
在正常情况下,Activity 的常用生命周期就只有如下 7 个
- onCreate():表示 Activity 正在被创建,常用来初始化工作,比如调用 setContentView 加载界面布局资源,初始化 Activity 所需数据等;
- onRestart():表示 Activity 正在重新启动,一般情况下,当前 Acitivty 从不可见重新变为可见时,OnRestart 就会被调用;
- onStart():表示 Activity 正在被启动,此时 Activity 可见但不在前台,还处于后台,无法与用户交互;
- onResume():表示 Activity 获得焦点,此时 Activity 可见且在前台并开始活动,这是与 onStart 的区别所在;
- onPause():表示 Activity 正在停止,此时可做一些存储数据、停止动画等工作,但是不能太耗时,因为这会影响到新 Activity 的显示,onPause 必须先执行完,新 Activity 的 onResume 才会执行;
- onStop():表示 Activity 即将停止,可以做一些稍微重量级的回收工作,比如注销广播接收器、关闭网络连接等,同样不能太耗时;
- onDestroy():表示 Activity 即将被销毁,这是 Activity 生命周期中的最后一个回调,常做回收工作、资源释放;
延伸:从整个生命周期来看,onCreate 和 onDestroy 是配对的,分别标识着 Activity 的创建和销毁,并且只可能有一次调用; 从 Activity 是否可见来说,onStart 和 onStop 是配对的,这两个方法可能被调用多次; 从 Activity 是否在前台来说,onResume 和 onPause 是配对的,这两个方法可能被调用多次; 除了这种区别,在实际使用中没有其他明显区别;
谈一谈 Fragment 的生命周期?
Fragment 从创建到销毁整个生命周期中涉及到的方法依次为:onAttach()→onCreate()→onCreateView()→onActivityCreated()→onStart()→onResume()→onPause()→onStop()→onDestroyView()→onDestroy()→onDetach(),其中和 Activity 有不少名称相同作用相似的方法,而不同的方法有:
- onAttach():当 Fragment 和 Activity 建立关联时调用;
- onCreateView():当 fragment 创建视图调用,在onCreate 之后;
- onActivityCreated():当与 Fragment 相关联的 Activity 完成 onCreate()之后调用;
- onDestroyView():在 Fragment 中的布局被移除时调用;
- onDetach():当 Fragment 和 Activity 解除关联时调用;
谈谈消息机制 Handler 作用?有哪些要素?流程是怎样的?
- 负责跨线程通信,这是因为在主线程不能做耗时操作,而子线程不能更新 UI,所以当子线程中进行耗时操作后需要更新 UI 时,通过 Handler 将有关 UI 的操作切换到主线程中执行。
- 具体分为四大要素:
- Message(消息):需要被传递的消息,消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息。
- MessageQueue(消息队列):负责消息的存储与管理,负责管理由 Handler 发送过来的 Message。读取会自动删除消息,单链表维护,插入和删除上有优势。在其 next()方法中会无限循环,不断判断是否有消息,有就返回这条消息并移除。
- Handler(消息处理器):负责 Message 的发送及处理。主要向消息池发送各种消息事件(Handler.sendMessage())和处理相应消息事件(Handler.handleMessage()),按照先进先出执行,内部使用的是单链表的结构。
- Looper(消息池):负责关联线程以及消息的分发,在该线程下从 MessageQueue 获取 Message,分发给Handler,Looper 创建的时候会创建一个 MessageQueue,调用 loop()方法的时候消息循环开始,其中会不断调用 messageQueue 的 next()方法,当有消息就处理,否则阻塞在 messageQueue 的next()方法中。当 Looper 的 quit()被调用的时候会调用messageQueue 的 quit(),此时 next()会返回 null,然后 loop()方法也就跟着退出。
具体流程如下:
- 在主线程创建的时候会创建一个 Looper,同时也会在在 Looper 内部创建一个消息队列。而在创键 Handler 的时候取出当前线程的 Looper,并通过该 Looper 对象获得消息队列,然后 Handler 在子线程中通过 MessageQueue.enqueueMessage 在消息队列中添加一条 Message。
- 通过 Looper.loop() 开启消息循环不断轮询调用 MessageQueue.next(),取得对应的 Message 并且通过 Handler.dispatchMessage 传递给 Handler,最终调用 Handler.handlerMessage 处理消息。
一个线程能否创建多个 Handler,Handler 跟 Looper 之间的对应关系 ?
- 一个 Thread 只能有一个 Looper,一个 MessageQueen,可以有多个 Handler
- 以一个线程为基准,他们的数量级关系是: Thread(1) : Looper(1) : MessageQueue(1) : Handler(N)
Looper 死循环为什么不会导致应用卡死?
- 主线程的主要方法就是消息循环,一旦退出消息循环,那么你的应用也就退出了,Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生 ANR 异常。
- 造成 ANR 的不是主线程阻塞,而是主线程的 Looper 消息处理过程发生了任务阻塞,无法响应手势操作,不能及时刷新 UI。
- 阻塞与程序无响应没有必然关系,虽然主线程在没有消息可处理的时候是阻塞的,但是只要保证有消息的时候能够立刻处理,程序是不会无响应的。