Android - 面试题(1)
Activity 有几种 launch mode?每一种有什么特点?
-
standard(默认的启动模式)
当我们发送一个intent启动该Activity后,该Activity总是被创建一个新的实例。
-
singleTop(栈顶复用)
如果要启动 Activity 存在于任务栈中,并且位于任务栈的顶部,就不会创建新的 Activity 实例,而是会调用 该 Activity 的 onNewIntent() 方法, 避免栈顶的 Activity 被重复的创建。 如果被调用的Activity的实例已经存在但不位于栈顶,那这个被调用的activity依然会被创建。
-
singleTask (栈内复用)
Activity 只会在任务栈里面存在一个实例。如果要启动的 Activity 在任务栈 Task 中已经存在,就不会创建新的 Activity 实例,而是复用这个已经存在的 Activity,调用 onNewIntent() 方法,并且清空这个 Activity 任务栈之上所有的其他 Activity
-
singleInstance (单一实例模式)
设置了该模式的 Activity 只能位于一个单独的任务栈中,不能在有其他 Activity , 其他任何从该 Activity 启动的其他 Activity 都会放到其他的任务栈中。
Service 有几种类型?各有什么应用场景?
方法 | 启动方式 | 停止方式 | 与启动的组件通信方法 | 生命周期 |
---|---|---|---|---|
startService | 在其他组件中调用startService()方法后,服务即处于启动状态 | service中调用stopSelf()方法,或者其他组件调用stopService()方法后,service将停止运行 | 没有提供默认的通信方式,启动service后该service就处于独立运行状态 | 一旦启动,service即可在后台无限期运行,即使启动service的组件已被销毁也不受其影响,直到其被停止 |
bindService | 在其他组件中调用bindService()方法后,服务即处于启动状态 | 所有与service绑定的组件都被销毁,或者它们都调用了unbindService()方法后,service将停止运行 | 可以通过 ServiceConnection进行通信,组件可以与service进行交互、发送请求、获取结果,甚至是利用IPC跨进程执行这些操作 | 当所有与其绑定的组件都取消绑定(可能是组件被销毁也有可能是其调用了unbindService()方法)后,service将停止 |
广播有几种注册方式?有什么区别?
-
代码注册
非常驻型注册,跟随生命周期变化,及Activity不可见时取消注册。不过当BroadcastReceiver(receiver)需要更新UI的时候,一般会采用该注册广播的方法
-
通过androidmainfest.xml中注册
常驻行注册,简单的说就是即使关闭了应用程序,后台接收到消息通知,照样可以唤醒应用当中的响应程序
Activity 有哪些生命周期回调?
方法 | 简介 |
---|---|
onCreate | Activity创建后被调用的第一个方法,一般用来完成所有的静态设置,如中创建views,ListView绑定数据,如调用:setContentView(int layoutResID)findViewById(int id)(ps:如果是fragment中的控件,该方法返回null,fragment是在该方法之后执行的)Bundle参数可以用来恢复之前保持的状态数据,横竖屏切换的时候则会用到。该方法调用之后一般会自动调用onStart()方法 |
onRestart | Stopped状态的Activity重新被调用的时候调用该方法。如用户由其他Activity返回的时候。 |
onStart | activity被调到前台可见之前执行该方法。在创建onCreate后或者重新启动onRestart后调用。该方法执行后,一般情况下,系统会自动调用onResume()方法,异常情况,也有可能直接调用onStop()方法。 |
onResume | 调用该方法后,Activity处于前台可见、可交互状态,Activity处于running状态 |
onPause | Activity在被其他activity遮盖之前执行该方法;Activity处于Paused状态,在3.1之前的版本中在极端情况下回被系统killed;在这个方法中可以用来保存持久化数据、停止动画和其他消耗cpu的事情等,只能执行轻量级的快速操作。执行耗时的操作在onStop比较合适一些 |
onStop | Activity完全不可见的时候执行该方法,比如:新的Activity启动并覆盖了当前的activity,或者返回到前一个Activity。当前Activity处于Stopped,随时可能被系统销毁,也有可能被重新启动调到前台。异常情况,该方法可能不会执行,Activity直接被killed掉 |
onDestroy | Activity资源被系统回收之前执行的最后一个方法,调用finish()或者系统临时销毁Activity的时候调用,可以使用isFinishing()进行判断是正常销毁还是异常情况。用户改变设置(屏幕方向、语言、输入设备等)当前Activity实例会被销毁,然后重新创建一个新的实例;异常情况,该方法可能不会执行,Activity直接被killed掉 |
Kotlin 中的扩展函数是什么
kotlin中的扩展函数,实际上是在自己的类中添加了一个static final方法,将需要扩展的类,在使用的时候作为第一个参数传入方法中,然后使用:
fun String.hello(str: String): String{
return "hello" + str + this.length
}
fun String.hello1(str: String): String{
return "hello$str"
}
经过反编译之后生成字节码如下
public final class ExtensionTestKt {
@NotNull
public static final String hello(@NotNull String $receiver, @NotNull String str) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
Intrinsics.checkParameterIsNotNull(str, "str");
return "hello" + str + $receiver.length();
}
@NotNull
public static final String hello1(@NotNull String $receiver, @NotNull String str) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
Intrinsics.checkParameterIsNotNull(str, "str");
return "hello" + str;
}
}
kotlin中的扩展函数,实际上就是通过给类添加public static final函数的做法来实现,这样做可以减少utils类的使用。
扩展函数是静态解析的,是采用静态分派的过程来处理。这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的,而不是由表达式运行时求值结果决定的。这意思其实就是在使用该扩展函数的时候,如果类本身和其子类都进行了同一个函数的扩展,这函数是不会有重写关系的,在使用的时候,只会根据需要使用该方法的对象的实际类型来决定是调用了哪个,就是相当于调用静态方法。而不是动态分派。
JVM 内存模型是怎么样的?
其中方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的。
- 程序计数器
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
- Java栈(虚拟机栈)
同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型。
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,
方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程
- 本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)
服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,
我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码
- 堆
堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,
所以多线程的时候也需要同步机制
- 方法区
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候
就被加载到方法区中
GC 回收算法?
-
标记-清除算法
该算法先标记,后清除,将所有需要回收的算法进行标记,然后清除;这种算法的缺点是:效率比较低;标记清除后会出现大量不连续的内存碎片,这些碎片太多可能会使存储大对象会触发GC回收,造成内存浪费以及时间的消耗。
-
复制算法
复制算法将可用的内存分成两份,每次使用其中一块,当这块回收之后把未回收的复制到另一块内存中,然后把使用的清除。这种算法运行简单,解决了标记-清除算法的碎片问题,但是这种算法代价过高,需要将可用内存缩小一半,对象存活率较高时,需要持续的复制工作,效率比较低。
-
标记-整理算法
标记整理算法是针对复制算法在对象存活率较高时持续复制导致效率较低的缺点进行改进的,该算法是在标记-清除算法基础上,不直接清理,而是使存活对象往一端游走,然后清除一端边界以外的内存,这样既可以避免不连续空间出现,还可以避免对象存活率较高时的持续复制。这种算法适合老生代。
-
分代收集算法
分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以使用标记整理算法
内存分配与回收策略
如图所示是堆中内存分配示意图,创建一个对象,首先会在eden区域分配区域,如果内存不够,就会将年龄大的转移到Survivor区,当survivor区域存储不下,则会转移年老代的。对于一些静态变量不需要使用对象,直接调用的,则会被放入永生代。一般来说长期存活的对象最终会被存放到年老代,还有一种特殊情况也会被存放到年老代,就是创建大对象时,比如数据这种需要申请连续空间的,如果空间比较大的,则会直接进入年老代。
在回收过程中,有一个参数比较重要,就是对象的年龄,如果在一次垃圾回收过程中有使用该对象的,则将对象年龄加1,否则减1,当计数为0,则进行回收,如果年龄达到一定数字则进入老生代。总的来说内存分配机制主要体现在对象创建之后是否仍在使用,已经不使用的则回收,继续使用的则对其年龄进行更新,达到一定程度,转移到年老代。
Java 中有几种引用类型?
-
强引用
这种引用是平时开发中最常用的,例如
String strong = new String("Strong Reference")
,当一个实例对象具有强引用时,垃圾回收器不会回收该对象,当内存不足时,宁愿抛出OutOfMemeryError异常也不会通过回收强引用的对象,因为JVM认为强引用的对象是用户正在使用的对象,它无法分辨出到底该回收哪个,强行回收有可能导致系统严重错误 -
软引用
如果一个对象只有软引用,那么只有当内存不足时,JVM才会去回收该对象,其他情况不会回收。软引用可以结合ReferenceQueue来使用,当由于系统内存不足,导致软引用的对象被回收了,JVM会把这个软引用加入到与之相关联的ReferenceQueue中。
-
弱引用
只有弱引用的对象,当JVM触发gc时,就会回收该对象。与软引用不同的是,不管是否内存不足,弱引用都会被回收。弱引用可以结合ReferenceQueue来使用,当由于系统触发gc,导致软引用的对象被回收了,JVM会把这个弱引用加入到与之相关联的ReferenceQueue中,不过由于垃圾收集器线程的优先级很低,所以弱引用不一定会被很快回收。
-
虚引用
如果一个对象只有虚引用在引用它,垃圾回收器是可以在任意时候对其进行回收的,虚引用主要用来跟踪对象被垃圾回收器回收的活动,当被回收时,JVM会把这个虚引用加入到与之相关联的ReferenceQueue中。与软引用和弱引用不同的是,虚引用必须有一个与之关联的ReferenceQueue,通过phantomReference.get()得到的值为null
Android 消息机制是怎么实现的?
-
Handler通过sendMessage()发送Message到MessageQueue队列;
-
Looper通过loop(),不断提取出达到触发条件的Message,并将Message交给target来处理;
-
经过dispatchMessage()后,交回给Handler的handleMessage()来进行相应地处理。
-
将Message加入MessageQueue时,处往管道写入字符,可以会唤醒loop线程;如果MessageQueue中没有Message,并处于Idle状态,则会执行IdelHandler接口中的方法,往往用于做一些清理性地工作。
Android 触摸事件如何传递?
onInterceptTouchEvent
返回值true表示事件拦截,onTouch/onTouchEvent
返回值true表示事件消费。- 触摸事件先交由
Activity.dispatchTouchEvent
。再一层层往下分发,当中间的ViewGroup都不拦截时,进入最底层的View后,开始由最底层的OnTouchEvent
来处理,如果一直不消费,则最后返回到Activity.OnTouchEvent
。 - ViewGroup才有
onInterceptTouchEvent
拦截方法。在分发过程中,中间任何一层ViewGroup都可以直接拦截,则不再往下分发,而是交由发生拦截操作的ViewGroup的OnTouchEvent
来处理。 - 子View可调用
requestDisallowInterceptTouchEvent
方法,来设置disallowIntercept=true
,从而阻止父ViewGroup的onInterceptTouchEvent
拦截操作。 - OnTouchEvent由下往上冒泡时,当中间任何一层的OnTouchEvent消费该事件,则不再往上传递,表示事件已处理。
- 如果View没有消费ACTION_DOWN事件,则之后的ACTION_MOVE等事件都不会再接收。
- 只要
View.onTouchEvent
是可点击或可长按,则消费该事件. onTouch
优先于onTouchEvent
执行,上面流程图中省略,onTouch
的位置在onTouchEvent
前面。当onTouch
返回true,则不执行onTouchEvent
,否则会执行onTouchEvent。onTouch
只有View设置了OnTouchListener
,且是enable的才执行该方法。
Android 视图是怎么被绘制出来的?
Android 如何在不同组件间通信?(跨进程,跨线程)
同一进程内:
- 使用
intent
进行传值 - 使用
binder
传值(Activity
和Service
之间) - 使用
broadcast
广播进行传值 - 使用
Application,SharePreference
,文件存储,数据库,ContentProvider
等等 - 使用接口
- 使用事件总线的
Eventbus,rxbus,LiveDataBus
等。 跨进程: - Activity可以跨进程调用其他应用程序的Activity
- Content Provider可以跨进程访问其他应用程序中的数据(以Cursor对象形式返回)
- Broadcast可以向android系统中所有应用程序发送广播,而需要跨进程通讯的应用程序可以监听这些广播
- Service和Content Provider类似,也可以访问其他应用程序中的数据,但不同的是,Content Provider返回的是Cursor对象,而Service返回的是Java对象,这种可以跨进程通讯的服务叫AIDL服务。 跨线程: 通过消息机制
Activity 启动流程?
RecyclerView 缓存机制?
Recycleview有四级缓存,分别是mAttachedScrap(屏幕内),mCacheViews(屏幕外),mViewCacheExtension(自定义缓存),mRecyclerPool(缓存池)
mAttachedScrap(屏幕内)
,用于屏幕内itemview快速重用,不需要重新createView和bindViewmCacheViews(屏幕外)
,保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。mViewCacheExtension(自定义缓存)
,不直接使用,需要用户自定义实现,默认不实现。mRecyclerPool(缓存池)
,当cacheView满了后或者adapter被更换,将cacheView中移出的ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。
Android里面的帧动画是通过AnimationDrawable来实现的,是将所有帧的图片都解析出来,而且会占用内存,一旦动画较复杂帧数较多,在低配置手机上容易发生OOM。
优化的思路采用分次加载,SurfaceView
就可以做到。
如何防止搜索框过度频繁地发起请求?
- 当搜索框内容发生改变时等待1000毫秒,message携带内容通知handler进行处理
- 判断handler携带的数据和搜索框的内容是否相等。当handler携带的数据不为空并且和搜索框的内容相当时进行网络请求。
如何实现弹幕?
首先在布局中放置一个显示游戏界面的View,然后在游戏界面的上方再覆盖一个显示弹幕的View就可以了。弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有人发弹幕消息时,再将消息绘制到弹幕的View上面就可以了。
其次在弹幕的View上面再覆盖一个操作界面的View,然后我们就可以在操作界面上发弹幕、送礼物等
设计图片异步加载组件需要注意哪些方面?
- 单例实现,单例默认不传参数,当然也支持传参单例调用框架。
- 图片缓存管理
- 图片压缩
- 任务队列(每来一次新的加载图片的请求,封装成Task添加到的任务队列TaskQueue中去)
- 后台轮询线程:该线程在第一次初始化实例的时候启动,然后会一直在后台运行;当每来一次加载图片请求的时候,除了会创建一个新的任务到任务队列中去,同时发一个消息到后台线程,后台线程去使用线程池去TaskQueue去取一个任务执行
转载自:https://juejin.cn/post/7023726607408267272