新鲜出炉的 Android 面试题,确定不来看看吗?还有超详细的答案解析哦~
广播静态注册和动态注册的区别
动态注册广播不是常驻型广播,也就是说广播跟随 Activity 的生命周期。注意在 Activity 结束前,移除广播接收器。 静态注册是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。
当广播为有序广播时:优先级高的先接收(不分静态和动态)。同优先级的广播接收器,动态优先于静态
同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。
当广播为默认广播时:无视优先级,动态广播接收器优先于静态广播接收器。同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后册的。
Android 性能优化工具使用(这个问题建议配合 Android 中的性能优化)
Android 中常用的性能优化工具包括这些:Android Studio 自带的 Android Profiler、LeakCanary、BlockCanary
Android 自带的 Android Profiler 其实就很好用,Android Profiler 可以检测三个方面的性能问题:CPU、MEMORY、NETWORK。
LeakCanary 是一个第三方的检测内存泄漏的库,我们的项目集成之后 LeakCanary 会自动检测应用运行期间的内存泄漏,并将之输出给我们。
BlockCanary 也是一个第三方检测 UI 卡顿的库,项目集成后 Block 也会自动检测应用运行期间的 UI 卡顿,并将之输出给我们。
Android 中的类加载器
PathClassLoader,只能加载系统中已经安装过的 apk
DexClassLoader,可以加载
jar/apk/dex
,可以从 SD 卡中加载未安装的 apk
Android 中的动画有哪几类,它们的特点和区别是什么
Android 中动画大致分为 3 类:帧动画、补间动画(Tween Animation)、属性动画(Property Animation)。
帧动画:通过 xml 配置一组图片,动态播放。很少会使用。
补间动画(Tween Animation):大致分为旋转、透明、缩放、位移四类操作。很少会使用。
属性动画(Property Animation):属性动画是现在使用的最多的一种动画,它比补间动画更加强大。属性动画大致分为两种使用类型,分别是 ViewPropertyAnimator 和 ObjectAnimator。前者适合一些通用的动画,比如旋转、位移、缩放和透明,使用方式也很简单通过
View.animate()
即可得到 ViewPropertyAnimator,之后进行相应的动画操作即可。后者适合用于为我们的自定义控件添加动画,当然首先我们应该在自定义 View 中添加相应的getXXX()
和setXXX()
相应属性的 getter 和 setter 方法,这里需要注意的是在 setter 方法内改变了自定义 View 中的属性后要调用invalidate()
来刷新 View 的绘制。之后调用ObjectAnimator.of
属性类型()返回一个 ObjectAnimator,调用start()
方法启动动画即可。
补间动画与属性动画的区别:
补间动画是父容器不断的绘制 view,看起来像移动了效果,其实 view 没有变化,还在原地。
是通过不断改变 view 内部的属性值,真正的改变 view。
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 的分析着重在两个部分:发送消息和处理消息。
发送消息。其实发送消息除了 sendMessage 之外还有 sendMessageDelayed 和 post 以及 postDelayed 等等不同的方式。但它们的本质都是调用了 sendMessageAtTime。在 sendMessageAtTime 这个方法中调用了 enqueueMessage。在 enqueueMessage 这个方法中做了两件事:通过
msg.target = this
实现了消息与当前 handler 的绑定。然后通过queue.enqueueMessage
实现了消息入队。处理消息。 消息处理的核心其实就是
dispatchMessage()
这个方法。这个方法里面的逻辑很简单,先判断msg.callback
是否为 null,如果不为空则执行这个 runnable。如果为空则会执行我们的handleMessage
方法。
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 的体积。
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"
这个属性,另一个就是同一个应用开启多个进程来扩大一个应用的总内存空间。第二种方法其实就很常见了,比方说我使用过个推的 S DK,个推的 Service 其实就是处在另外一个单独的进程中。
Android 中的内存优化总的来说就是开源和节流,开源就是扩大内存,节流就是避免内存泄漏。
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 通过 inder 驱动返回服务端的代理对象。
客户端拿到服务端的代理对象后即可进行进程间通信。
LruCache 的原理
LruCache 的核心原理就是对 LinkedHashMap 的有效利用,它的内部存在一个 LinkedHashMap 成员变量。值得我们关注的有四个方法:构造方法、get、put、trimToSize。
构造方法: 在 LruCache 的构造方法中做了两件事,设置了 maxSize、创建了一个 LinkedHashMap。这里值得注意的是 LruCache 将 LinkedHashMap 的 accessOrder 设置为了 true,accessOrder 就是遍历这个 LinkedHashMap 的输出顺序。true 代表按照访问顺序输出,false 代表按添加顺序输出,因为通常都是按照添加顺序输出,所以 accessOrder 这个属性默认是 false,但我们的 LruCache 需要按访问顺序输出,所以显式的将 accessOrder 设置为 true。
get 方法: 本质上是调用 LinkedHashMap 的 get 方法,由于我们将 accessOrder 设置为了 true,所以每调用一次 get 方法,就会将我们访问的当前元素放置到这个 LinkedHashMap 的尾部。
put 方法: 本质上也是调用了 LinkedHashMap 的 put 方法,由于 LinkedHashMap 的特性,每调用一次 put 方法,也会将新加入的元素放置到 LinkedHashMap 的尾部。添加之后会调用 trimToSize 方法来保证添加后的内存不超过 maxSize。
trimToSize 方法: trimToSize 方法的内部其实是开启了一个 while(true)的死循环,不断的从 LinkedHashMap 的首部删除元素,直到删除之后的内存小于 maxSize 之后使用 break 跳出循环。
其实到这里我们可以总结一下,为什么这个算法叫 最近最少使用 算法呢?原理很简单,我们的每次 put 或者 get 都可以看做一次访问,由于 LinkedHashMap 的特性,会将每次访问到的元素放置到尾部。当我们的内存达到阈值后,会触发 trimToSize 方法来删除 LinkedHashMap 首部的元素,直到当前内存小于 maxSize。为什么删除首部的元素,原因很明显:我们最近经常访问的元素都会放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了,因此当内存不足时应当优先删除这些元素。
DiskLruCache 原理
设计一个图片的异步加载框架
设计一个图片加载框架,肯定要用到图片加载的三级缓存的思想。三级缓存分为内存缓存、本地缓存和网络缓存。
内存缓存:将 Bitmap 缓存到内存中,运行速度快,但是内存容量小。 本地缓存:将图片缓存到文件中,速度较慢,但容量较大。 网络缓存:从网络获取图片,速度受网络影响。
如果我们设计一个图片加载框架,流程一定是这样的:
拿到图片 url 后首先从内存中查找 BItmap,如果找到直接加载。
内存中没有找到,会从本地缓存中查找,如果本地缓存可以找到,则直接加载。
内存和本地都没有找到,这时会从网络下载图片,下载到后会加载图片,并且将下载到的图片放到内存缓存和本地缓存中。
上面是一些基本的概念,如果是具体的代码实现的话,大概需要这么几个方面的文件:
首先需要确定我们的内存缓存,这里一般用的都是 LruCache。
确定本地缓存,通常用的是 DiskLruCache,这里需要注意的是图片缓存的文件名一般是 url 被 MD5 加密后的字符串,为了避免文件名直接暴露图片的 url。
内存缓存和本地缓存确定之后,需要我们创建一个新的类 MemeryAndDiskCache,当然,名字随便起,这个类包含了之前提到的 LruCache 和 DiskLruCache。在 MemeryAndDiskCache 这个类中我们定义两个方法,一个是 getBitmap,另一个是 putBitmap,对应着图片的获取和缓存,内部的逻辑也很简单。getBitmap 中按内存、本地的优先级去取 BItmap,putBitmap 中先缓存内存,之后缓存到本地。
在缓存策略类确定好之后,我们创建一个 ImageLoader 类,这个类必须包含两个方法,一个是展示图片
displayImage(url,imageView)
,另一个是从网络获取图片downloadImage(url,imageView)
。在展示图片方法中首先要通过ImageView.setTag(url)
,将 url 和 imageView 进行绑定,这是为了避免在列表中加载网络图片时会由于 ImageView 的复用导致的图片错位的 bug。之后会从 MemeryAndDiskCache 中获取缓存,如果存在,直接加载;如果不存在,则调用从网络获取图片这个方法。从网络获取图片方法很多,这里我一般都会使用OkHttp+Retrofit
。当从网络中获取到图片之后,首先判断一下imageView.getTag()
与图片的 url 是否一致,如果一致则加载图片,如果不一致则不加载图片,通过这样的方式避免了列表中异步加载图片的错位。同时在获取到图片之后会通过 MemeryAndDiskCache 来缓存图片。
Android 中的事件分发机制
在我们的手指触摸到屏幕的时候,事件其实是通过 Activity -> ViewGroup -> View
这样的流程到达最后响应我们触摸事件的 View。
说到事件分发,必不可少的是这几个方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。
接下来就按照Activity -> ViewGroup -> View
的流程来大致说一下事件分发机制。
我们的手指触摸到屏幕的时候,会触发一个 Action_Down 类型的事件,当前页面的 Activity 会首先做出响应,也就是说会走到 Activity 的 dispatchTouchEvent()
方法内。在这个方法内部简单来说是这么一个逻辑:
调用
getWindow.superDispatchTouchEvent()。
如果上一步返回 true,直接返回 true;否则就 return 自己的
onTouchEvent()。
这个逻辑很好理解,getWindow().superDispatchTouchEvent()
如果返回 true 代表当前事件已经被处理,无需调用自己的 onTouchEvent;否则代表事件并没有被处理,需要 Activity 自己处理,也就是调用自己的 onTouchEvent。
getWindow()
方法返回了一个 Window 类型的对象,这个我们都知道,在 Android 中,PhoneWindow 是 Window 的唯一实现类。所以这句本质上是调用了``PhoneWindow 中的 superDispatchTouchEvent()。`
而在 PhoneWindow 的这个方法中实际调用了mDecor.superDispatchTouchEvent(event)
。这个 mDecor 就是 DecorView,它是 FrameLayout 的一个子类,在 DecorView 中的 superDispatchTouchEvent()
中调用的是 super.dispatchTouchEvent()
。到这里就很明显了,DecorView 是一个 FrameLayout 的子类,FrameLayout 是一个 ViewGroup 的子类,本质上调用的还是 ViewGroup的dispatchTouchEvent()
。
分析到这里,我们的事件已经从 Activity 传递到了 ViewGroup,接下来我们来分析下 ViewGroup 中的这几个事件处理方法。
在 ViewGroup 中的 dispatchTouchEvent()
中的逻辑大致如下:
通过
onInterceptTouchEvent()
判断当前 ViewGroup 是否拦截事件,默认的 ViewGroup 都是不拦截的;如果拦截,则 return 自己的
onTouchEvent()
;如果不拦截,则根据
child.dispatchTouchEvent()
的返回值判断。如果返回 true,则 return true;否则 return 自己的onTouchEvent()
,在这里实现了未处理事件的向上传递。
通常情况下 ViewGroup 的 onInterceptTouchEvent()
都返回 false,也就是不拦截。这里需要注意的是事件序列,比如 Down 事件、Move 事件…Up 事件,从 Down 到 Up 是一个完整的事件序列,对应着手指从按下到抬起这一系列的事件,如果 ViewGroup 拦截了 Down 事件,那么后续事件都会交给这个 ViewGroup 的 onTouchEvent。如果 ViewGroup 拦截的不是 Down 事件,那么会给之前处理这个 Down 事件的 View 发送一个 Action_Cancel 类型的事件,通知子 View 这个后续的事件序列已经被 ViewGroup 接管了,子 View 恢复之前的状态即可。
这里举一个常见的例子:在一个 Recyclerview 钟有很多的 Button,我们首先按下了一个 button,然后滑动一段距离再松开,这时候 Recyclerview 会跟着滑动,并不会触发这个 button 的点击事件。这个例子中,当我们按下 button 时,这个 button 接收到了 Action_Down 事件,正常情况下后续的事件序列应该由这个 button 处理。但我们滑动了一段距离,这时 Recyclerview 察觉到这是一个滑动操作,拦截了这个事件序列,走了自身的 onTouchEvent()
方法,反映在屏幕上就是列表的滑动。而这时 button 仍然处于按下的状态,所以在拦截的时候需要发送一个 Action_Cancel 来通知 button 恢复之前状态。
事件分发最终会走到 View 的 dispatchTouchEvent()
中。在 View 的 dispatchTouchEvent()
中没有 onInterceptTouchEvent()
,这也很容易理解,View 不是 ViewGroup,不会包含其他子 View,所以也不存在拦截不拦截这一说。忽略一些细节,View 的 dispatchTouchEvent()
中直接 return 了自己的 onTouchEvent()
。如果 onTouchEvent()
返回 true 代表事件被处理,否则未处理的事件会向上传递,直到有 View 处理了事件或者一直没有处理,最终到达了 Activity 的 onTouchEvent()
终止。
这里经常有人问 onTouch 和 onTouchEvent 的区别。首先,这两个方法都在 View 的 dispatchTouchEvent()
中,是这么一个逻辑:
如果 touchListener 不为 null,并且这个 View 是 enable 的,而且 onTouch 返回的是 true,满足这三个条件时会直接 return true,不会走
onTouchEvent()
方法。上面只要有一个条件不满足,就会走到
onTouchEvent()
方法中。所以 onTouch 的顺序是在 onTouchEvent 之前的。
View 的绘制流程
视图绘制的起点在 ViewRootImpl 类的 performTraversals()
方法,在这个方法内其实是按照顺序依次调用了 mView.measure()、mView.layout()、mView.draw()
View 的绘制流程分为 3 步:测量、布局、绘制,分别对应 3 个方法 measure、layout、draw。
测量阶段。 measure 方法会被父 View 调用,在 measure 方法中做一些优化和准备工作后会调用 onMeasure 方法进行实际的自我测量。onMeasure 方法在 View 和 ViewGroup 做的事情是不一样的:
View。 View 中的 onMeasure 方法会计算自己的尺寸并通过 setMeasureDimension 保存。
ViewGroup。 ViewGroup 中的 onMeasure 方法会调用所有子 iew 的 measure 方法进行自我测量并保存。然后通过子 View 的尺寸和位置计算出自己的尺寸并保存。
评论