有些基础性的知识,虽然知道有这个方法,但是想在面试的时候比较系统顺利表达出来,还是有点难度的。所以做这个记录,希望自己没事的时候多看看。
请简述 Handler 的工作原理
Handler 有两个重要的组成部分,Looper(消息轮询器) 和 MessageQueue(消息队列)
Looper 是 Handler 实现的核心,Looper 在构造方法中会创建 MessageQueue,而 Handler 处理消息的时候会交给 MessageQueue.enqueueMessage 方法,而 MessageQueue 会将消息链表进行重新排序,再判断 Looper 是否唤醒,如果 MessageQueue 中没有正在消息轮询,那么 Looper 会一直处于休眠状态,所以 MessageQueue 需要触发 Looper 对自己的轮询。(通过往管道写入数据通知 Looper.looper)
请简述数据存储有哪几种方式,并且描述在实际项目中的应用
大致分为两种:
1. 本地存储(文件、SP、SQLite、MMKV)
2. 网络存储(普通传输、加密传输)
在实际开发中,我们会将一些配置文件存放在本地,比如应用设置,用户信息等保存到本地中,而一些比较重要的数据,则通过接口发送给后台保存,对于登陆密码或者支付密码还需要进行加密操作。
Activity 四种启动模式
standard(标准模式):拿来主义,start 多少次就创建多少个 Activity
singleTop(单一栈顶):如果欲启动的 Activity 是栈顶,则不会重复创建。
singleTask(单一任务栈):如果欲启动的 Activity 有实例在任务栈,就会将在这个 Activity 上层的 Activity 清除,这样即能保证栈中的唯一性,又能达到复用的效果。
singleInstance(单一实例):这种模式比较特殊,Activity 会运行在单独的任务栈中,整个手机中只有一个实例存在。
请简述原生和 H5 交互的流程:
首先开启支持 JavaScript ,并且加载一遍网页,这个是前提条件
原生调用 H5:通过 loadUrl 来调用 JS 方法 (WebView.loadUrl("javascript:xxxxx");)
H5 调用原生:创建一个普通的方法并且添加注解(@JavascriptInterface),然后调用 addJavascriptInterface 将这个方法注入给 WebView
适配屏幕:
现在主流的屏幕适配其实有两种方式:一种是使用原生 dp sp 的方式进行适配,而这种适配方式的思想在于在大的屏幕显示多一点内容,由此来追求更好的显示效果,这种方式适配比较简单。而另一种是使用今日头条的适配方案,这种适配方式跟原生的方式的思想不一样,采用百分比的方式进行适配,力求在每个屏幕的显示效果一致,不同设备的显示差异最小化。
关于这种两种方式的取舍,我个人的建议是,如果 APP 不适配平板的情况下,采用原生的方式,如果需要适配平板,可以考虑头条的适配方案。
简述 Java 最常见的两种查找算法:
1. 线性查找:这个,对数组进行遍历操作
2. 二分法查找:这个算法必须要数组是有序的前提下,取数组中间的值进行比较,如果小于则取左边的数值进行查找(否则向右查找),再获取左边的数值中间的值进行比较,以此类推,直到找到对应的值为止。
最常用的三种的图片格式(jpg、png、webp)分别有什么区别
jpg:有损压缩格式,体积比较小,不支持透明通道
png:无损压缩格式,体积比较大,支持透明通道
webp:Google 发布的图片格式,压缩率比 jpg 更好,jpg 和 webp 压缩出来的图片大小相差无几,但是 webp 的图片质量比 jpg 要高
Glide 和 Picasso 框架的区别:
1. Glide 功能比 Picasso 要更加强大(Glide 支持加载 Gif 和视频缩略图,而 Picasso 不支持)
2. Glide 的库体积比 Picasso 要大(Glide 600KB,Picasso 100KB)
3. Glide 的本地缓存比 Picasso 要大(Glide 缓存的是合适尺寸的图片,而 Picasso 只缓存了原图)
4. Glide 比 Picasso 加载中更省内存(Glide 是直接加载本地缓存的图片,而 Picasso 要经过尺寸压缩才会进行加载)
设计模式:
观察者设计模式:角色分为观察者和被观察者,被观察者发生变化之后通过接口通知观察者。其中一个被观察者可对应一个或者多个观察者对象,View 点击监听就是一对一,而 EditText 文本监听就是一对多。
单例设计模式:单例即是在内存中只有一份实例,可以通过两种方式创建,第一种是饿汉式,在使用到类的时候就已经初始化了,第二种懒汉式,在获取对象的时候再创建。这两个方式最大区别是,懒汉式需要考虑线程安全的问题,而饿汉式不需要。
建造者设计模式:对本体和参数进行隔离封装。比如 v7 包下的 Dialog 就是采用了这种模式,通过 Builder 类构建一系列参数,整个过程而不涉及 Dialog 对象,这种设计模式在代码上还有一个特点,就是链式调用(参数简单明了),但是链式调用不一定就是采用了建造者设计模式。
Dalvik 和 ART 虚拟机的机制,两种有什么区别
Dalvik:这种虚拟机在应用每次运行的时候都会把字节码转换成机器码再执行,所以这种机制的运行效率比较低下,但是安装速度比较快。
ART:由于 Dalvik 在运行效率低下,于是谷歌在 Android 5.0 开始使用 ART,这种虚拟机在 apk 安装的时候会先将字节码转换成机器码(预编译过程),所以这种机制的安装效率比较低下,但是运行速度会比较快,同时也比较省电。
区别:这两种虚拟机优缺点刚好相反的,Dalvik 是拿时间换空间,而 ART 是拿空间换时间。在手机硬件基础日渐强大的情况下,手机内存的读写速度也越来越快,ART 的优势也越来越明显。
什么是内存抖动?如何避免内存抖动?
概念:内存抖动是因为短时间内有大量的对象进出(创建和回收),随着系统频繁的 GC,使渲染(UI)线程被阻塞,从而导致程序显示的画面有短暂卡顿。
避免:尽量避免在频繁调用的地方 new 对象(比如循环递归,View.onDraw、RecyclerView.onBindView),如果需要最好把对象提取到外层(循环外或者提取成字段),针对一些可复用的对象(比如 Bitmap),建议使用对象池进行缓存。
简述面向对象三大特性
封装:对类的可见性进行封装,从而决定类的访问权限。在 Java 上表现为权限修饰符,public 就是对所有类开放,protected 仅对子类开放,default仅对相同包名的类开放(在开发中,没有任何访问修饰符就为 default),private 仅对本类开放
继承:让类和类之间存在关系,子类拥有父类的特性,子类可以在父类的基础上面扩展。
多态:最大的表现为面向接口编程,尤其类的继承关系在变得复杂之后,多态的特性就显得尤为重要了,接口更像是一种规范,因为我们可以利用接口作为对象的类型,而不需要关心谁实现了它。
请简述 View 绘制流程
一般 View 绘制要经过三个步骤:测量、摆放、绘制
onMeasure:View 会测量自身的宽高、ViewGroup 会测量子 View 的宽高,其中测量有两个主要的值,测量模式和测量大小,这两个值其实由一个 32 位 int 值组成,高 2 位为测量模式,低 30 位为测量大小。而测量模式有三种:自适应(wrap_content)、精确值(match_parent、固定值)、未指明(View 想多大就多大,在 ListView、ScrollView 等在测量子布局的时候会用)。
onLayout:这个是 ViewGroup 独有的方法,用于决定子控件的位置,同时也可以对 View 的宽高进行调整。
onDraw:这个是 View 独有的方法,当 View 调用 invalidate 会先触发 draw 方法,然后依次绘制背景、内容、前景、滚动条。而我们平时最常用的 onDraw 方法就是这四个步骤的其中一个:绘制内容。
总结:onMeasure 得出是绘制区域的大小,onLayout 得出是绘制区域的位置,而 onDraw 是对指定区域进行绘制,这样 View 就能根据我们想要的方式绘制到屏幕上了。
请简述 View 事件分发机制
View 事件一般要经过三个流程:分发(dispatchTouchEvent)、拦截(onInterceptTouchEvent)、消费(onTouchEvent)
dispatchTouchEvent:View 会先执行 OnTouchListener 中 onTouch 方法,如果这个监听器的方法返回 true 则不会将事件交由自身的 onTouchEvent 进行消费。而 ViewGroup 跟 View 不同的是,它是直接将事件交由触摸位置上的子 View 的 dispatchTouchEvent 方法处理。
onInterceptTouchEvent:这个是 ViewGroup 独有的方法,在 dispatchTouchEvent 方法中调用,一般情况下返回 false(也就是不拦截),ViewGroup 如果想拦截触摸事件可以重写此方法返回 true,将事件交由自身的 onTouchEvent 方法处理。
onTouchEvent:一次触摸事件会产生 down(按下)、move(移动)、up(抬起)三个事件,这个方法将决定事件产生的效果,比如状态选择器的 Drawable 变换就是在这个方法中触发的,还有最常用的点击事件 onClick 也是在这里回调的。
总结:三个事件方法各有各的作用,分发是想把事件传递下去,拦截是不希望事件传递下去,而消费是对事件的一系列处理。通过这三个方法,我们可以控制触摸事件交由谁去处理,整个事件机制采用了责任链设计模式,事件会一层一层往下传递。
ANR 是什么,如何避免它
ANR 的全称是 Application Not Responding,中文是应用程序没有响应的意思。
那么我们在实际开发中如何避免这个问题,首先我们要从四大组件最大的耗时说起:
1. Activity:输入事件(按键和触摸事件)5s 内没被处理
2. BroadcastReceiver:在 10s 内发送的广播没有处理完成
3. Service:前台 Service( 20s 内)后台 Service(200s 内)没有完成启动
4. ContentProvider:数据库变化发布没有在 10s 内进行完
首先这四个组件都是在主线程中执行的,所以不能做耗时操作(比如请求网络、操作 IO),造成 ANR 主要的原因这些耗时操作是直接在主线程中完成,我们应该把这些耗时操作放到子线程中来,这样就能有效避免 ANR。
简述一下 ANR 实现原理
ANR 检测是由系统服务来完成,每当主线程接收到操作之后,系统会使用 Handler 会发送一个延迟消息,当这个操作完成之后会将这个延迟消息移除,如果这个延迟消息没有被移除,那么就证明应用没有及时响应,同时也会触发系统向用户发送 ANR 警告。
触发整个 ANR 过程可分为三个步骤:埋炸弹、拆炸弹、引爆炸弹
api 和 implementation 有什么区别
从 Gradle 3.4 开始,compile 已经过时,取而代之的是 api 和 implementation,两者最大的区别是依赖程度的不同,api 和 compile 一样是强依赖,当主模块使用 api 依赖库时,这个库所依赖的库中类是可见的,而 implementation 是弱依赖,刚好和 api 相反。在编译时间上看, implementation 相对 api 会快一些,因为它不需要对库中依赖的库进行编译时检查,所以一般情况下我们选用 implementation 来依赖第三方库
你用 Gradle 在实际开发中做过什么事
1. 通用配置抽取(版本号统一,AppCompat 集成)
2. Apk 签名配置(配置 debug 和 release 包的签名信息)
3. Apk 输出名称(配置 Apk 输出的名称,默认是 app_debug.apk)
4. 多渠道打包(配置第三方应用市场名称,用于统计用户渠道及投放广告)
5. 清单文件占位符(编译时替换清单文件中的字符,例如 ${applicationId} )
6.常用KEY配置
7.greenDao数据库版本控制
dp、sp、px 这些单位有什么区别
dp:以屏幕密度为基准,屏幕密度越大值越大,在开发中作为宽高适配的单位
sp:以手机设置的字体大小(小、正常、大、超大)为基准,字体越大值也越大,在开发中作为文字适配的单位
px:1px 等于 1 个像素点,相比前面两种没有任何适配的效果。在实际开发中,我们可以用 0px 来代替 0dp。
三级缓存是哪三级,分别有什么用
内存缓存:速度快,优先读取,但要管理好内存
本地缓存:速度其次,内存中没有,才读本地
网络缓存:速度最慢,本地也没有,才访问网络
Java 回收算法
1. 引用计数算法:对象引用对象会进行计数,当这个对象没有被任何对象引用的时候,计数就为零,这个对象就会被回收掉。但是这种算法是有缺陷的,当两个对象相互引用的时候,会导致对象无法回收。
2. GC Root 可达分析算法:由于引用计数算法是有问题的,后面诞生了这种回收算法。当 GC Root (栈帧中的变量、静态变量、常量、JNI 引用的对象)不可用时,其他引用这个 GC Root 的对象也将作为无效对象被垃圾收集器回收。
请简述内存溢出和内存泄漏有什么区别
内存溢出是系统给应用分配的内存使用超标导致的,而分配的内存大小是根据屏幕大小而定的,一般屏幕越大分配的内存越大,我们需要做好内存优化和内存控制;而内存泄漏是指 GC 垃圾回收器无法回收对象,导致对象占用的内存空间无法释放,我们可以理解成这部分内存空间被占用着,无法被系统重复利用。
代码优化
1. 对常用的功能模块进行封装
2. 对重复的代码考虑进行抽取
3. 关注编译器给出的警告并正确处理
4. 通过不断改进来优化代码的写法
5. 使用 AOP 降低一些代码的耦合性
6. 代码命名和文件命名要规范(阿里代码规范手册,驼峰命名)
7. 减少不必要的代码注释,尽量用规范的代码代替注释
8. 完善重点难点代码的注释,完善后台接口代码的注释(接口作用,参数含义)
9. 代码的摆放顺序要有一定的规律(根据代码类型和执行顺序来定义)
10. 将 RecyclerView 适配器中的 onBindViewHolder 优化到 ViewHolder 中(不需要再通过 ViewHolder 对象去操作 itemView,在遇到不同 type 的 ViewHolder 对象无需强转)
11.多用设计模式解决开发中遇到的问题
在讲布局优化之前,先跟大家讲一下,我自己平时是怎么写布局的,不知道大家有没有发现 AndroidProject 的代码中没有用到约束布局,用的最多是线性布局和帧布局,现在跟大家讲讲我的想法吧。我其实不喜欢约束布局,因为它除了布局绘制性能会比原生的布局要好,其他方面真的不行。比如约束布局的设计理念是为了减少布局之间的嵌套,但是它会使我们的布局代码逻辑变得复杂,无论是从首次开发还是后续维护上面无疑增加了成本。所以我不推荐大家用它,推荐大家用线性布局和帧布局。
我现在列举约束布局几处不好的地方:
1. 布局属性比较多并复杂,前期学习成本较高
2. 布局摆放没有层次概念,并且会产生许多适配碎片化的问题(View 与 View 之间约束不够严谨导致的越界)
3. 因为 View 与 View 之间的约束关系,要为一些不会在 Java 代码中使用到的 View 命名 id
4. 因为 View 与 Layout 之间的约束关系,要在布局中定义很多 Guideline(参照线)和 Space(间隙)
5. 从事件分发的机制上思考,它其实违背了责任链模式的思想,因为布局没有层次关系,只有相互约束关系。
6. 从布局变化上思考,它只能做到隐藏和显示简单的布局变化,一旦涉及 View 位置变化,代码逻辑将会十分复杂,甚至要重新布局中 View 之间的约束关系,而原生布局可以通过 removeView 和 addView 来完成这一操作。
约束布局有好有坏,原生布局有好有坏,但是我觉得原生布局总体上比约束布局要好,从 Glide 将图片质量从 RGB 565 提高到 ARGB 8888 看,无疑性能不再是我们第一要考虑的问题了,如果要以牺牲其他方面作为代价来换取的,需要我们经过认真的斟酌。在我看来,从代码设计能力来看,约束布局远远不如原生布局,它虽然能减少绘制带来的性能消耗,但是同时也会带来代码变复杂的问题。从这个方面上,我是不同意这样做的,因为未来手机的性能只会越来越好,从这一点上思考,我们更应该关注代码设计能力,这样我们写出来的代码质量才能更高。
布局优化
1. 常用控件样式抽取成 Style
2. 减少布局嵌套,复杂的布局使用约束布局
3. 多使用抽象布局(include、merge、ViewStub)
4. 多使用 tools 命名空间(text、src、listitem、context)
内存优化
1. 不用的对象及时释放,即指向 null
2. 在遍历得到想要的位置之后要跳出循环
3. 尽量不要在循环或者递归中 new 对象
4. 在频繁 new 对象的地方使用享元设计模式
5. 第三方框架懒加载或者使用到的时候再初始化
6. 尽量使用 Parcelable(通过接口序列化),减少使用 Serializable(反射次数多)
7. 减少避免使用反射(EventBus APT 插件可以减少反射次数)
8. 减少 findViewById 次数(不要在 onBindViewHolder 中进行 findViewById,而是在 ViewHolder 构造函数中进行)
9. 在数据量小的时候,应该选用 ArrayMap 和 SparseArray 来代替 HashMap(因为它们的数据结构很简单,都是由数组组成,而 HashMap 涉及了链表和红黑树)
10. 在 ViewPager 中 Fragment 比较少的情况下,应当使用 FragmentPagerAdapter,否则应当使用 FragmentStatePagerAdapter( FragmentPagerAdapter 只是简单走了一遍 Fragment生命周期,并没有真正从 ViewPager 里面移除掉,而 FragmentStatePagerAdapter 会保存 Fragment 的状态,然后把它从 ViewPager 中移除掉,然后下次使用的时候重新添加并且恢复它原有的状态。可以看出 FragmentStatePagerAdapter 对 Fragment 内存管理机制还是做得比较完善的)
11. 及时释放资源(IO、SQLite)
12. 监听器反注册(EditText、BroadcastReceiver)
13. WebView 生命周期优化(回调 onResume、onPause、Destroy)
14. WebView 独立进程优化(WebView 本身就是一个复杂的 View,消耗内存和性能比较大,放在独立进程中可以减小 APP 有一定几率因为内存溢出导致的崩溃)
如何避免内存泄漏:
出现内存的泄漏最多的两个原因:
1. 使用静态的 Activity
2. 不恰当使用 Handler
解决这两个内存泄漏的方案:
1. 尽量使用 Application 作为静态 Context 对象,如果一定要用 Activity,不能直接引用,而是需要使用弱引用或者软引用
2. Handler 是开发中最常用的类,伴随使用频率的增高,内存泄漏发生的情况也比较多,往往是我们使用不当导致的。我们应当在 Activity 销毁的时候 removeAllMessage,又或者将 Activity 弱引用或者软引用持有。
另外我们可以用第三方框架 LeakCanary(金丝鸟)来检测应用是否发生内存泄漏,这个框架的原理是通过监听 Activity 的生命周期,当 Activity 销毁之后一段时间,金丝鸟会检查弱引用中的 Activity 对象是否被置空(回收)了,如果是的话就证明这个 Activity 在使用的时候没有产生内存泄漏。
IntentService 和 Service 有什么区别:
1. Service 运行在主线程,而 IntentService 运行子线程
2. Service 必须手动调用 stopSelf 才能关闭服务,而 IntentService 执行完毕之后会自动关闭服务
抽象类和接口的区别:
从面向对象的继承特性上看,Java 支持单继承多实现,所以抽象类只能被子类继承,而接口可以被不同的类实现。由此可以得出,接口的扩展性要比抽象类要强。
抽象类可以有字段,而接口只能有常量字段。
抽象类中必须要有一个抽象方法,而接口没有这一要求。
抽象方法子类必须要实现,而接口方法则不一定,在 Java JDK 1.7 的时候,接口的方法是不能有默认实现的,而到了 Java JDK 1.8 ,就已经开始支持这特性。
new Message 和 Message.obtain() 区别
这两种最本质的区别是,第一种是通过 new 的形式创建一个 Message 对象,而另一种是通过复用的形式来获取一个 Message 对象。在使用 Handler 发送 Message 的情况下,建议采用第二种方式,因为第二种方式在消息频繁的情况下,所表现的性能较优,如果每次都创建 Message 对象,会造成不必要的资源浪费。
final、finally、finalize 的区别:
final:Java 修饰符,声明类不可继承,方法不可重写,属性不可变
finally:异常处理的一部分,表示代码块无论有没有 catch 都会执行
finalize:Object 的一个用于通知对象回收的方法,可在此处释放资源
APK 瘦身三部曲:
1. dex 优化:代码混淆,移除无用代码
2. 资源优化:只保留一套图 xxhdpi(1920 * 1080),图片压缩(在线压缩网站 TinyPng),图片转换(使用 webp 代替 png,使用 xml 矢量图作为 icon,使用 lottie 动画代替 gif)
3. so 优化:只保留主流架构 armeabi-v7a (armeabi 这种万金油架构已经不常见)
4. 其他优化:开启语种精简(Support 包),开启 zipAlign 压缩对齐
请简述 HashMap 的原理:
数据结构:HashMap 其实是一个对象数组,数据结构采用的是链表,当链表长度大于 8个 的时候,会切换成红黑树,如果红黑树长度小于 6 个会回退到链表。
存储流程:HashMap 是先计算 Key 对象的 hashCode 值,因为 hashCode 的值比较大,所以 HashMap 会用位运算对这个值压缩到 16 (对象数组长度)以内的值,得出来的结果就是链表的位置。
容量扩容:HashMap 默认的数组容量是 16,其负载因子是 0.75,如果超过了 12 (16 * 0.75)个元素,会对数组进行双倍扩容,也就是 32 (16 * 2)。扩容的过程比较简单,但扩容是一个密集操作,HashMap 会重新计算每个元素的位置,然后给这些元素重新排序。
Activity 的生命周期
相信不少朋友也已经看过这个流程图了,也基本了解了Activity生命周期的几个过程,我们就来说一说这几个过程。
1.启动Activity:系统会先调用onCreate方法,然后调用onStart方法,最后调用onResume,Activity进入运行状态。
2.当前Activity被其他Activity覆盖其上或被锁屏:系统会调用onPause方法,暂停当前Activity的执行。
3.当前Activity由被覆盖状态回到前台或解锁屏:系统会调用onResume方法,再次进入运行状态。
4.当前Activity转到新的Activity界面或按Home键回到主屏,自身退居后台:系统会先调用onPause方法,然后调用onStop方法,进入停滞状态。
5.用户后退回到此Activity:系统会先调用onRestart方法,然后调用onStart方法,最后调用onResume方法,再次进入运行状态。
6.当前Activity处于被覆盖状态或者后台不可见状态,即第2步和第4步,系统内存不足,杀死当前Activity,而后用户退回当前Activity:再次调用onCreate方法、onStart方法、onResume方法,进入运行状态。
7.用户退出当前Activity:系统先调用onPause方法,然后调用onStop方法,最后调用onDestory方法,结束当前Activity。
Fragment的生命周期图