常用面试题

#

面试题

  1. 深拷贝和浅拷贝的区别

浅拷⻉
对基本数据类型进⾏值传递、对引⽤数据类型进⾏引⽤传递的拷⻉。
深拷⻉
对基本数据类型进⾏值传递、对引⽤数据类型创建⼀个新的对象,并赋
值其内容

  1. 熟悉反射吗 如何提升反射的效率

1、缓存重复⽤到的对象
只要我们合理的运⽤这些反射⽅法,⽐如Class.forName,
Constructor,Method,Field等,尽量在循环外就缓存好实例,
就能提⾼反射的效率,减少耗时。
2、setAccessible(true)
我们在反射的过程中可以尽量调⽤setAccessible(true)来关闭安
全检查,这个安全检查其实也是耗时的,这样也能提⾼反射的效
率。
3、ReflectASM
是⼀个类似反射,但是不同于反射的⾼性能库。他的原理是通过
ASM库,⽣成了⼀个新的类,然后相当于直接调⽤新的类⽅法,
从⽽完成反射的功能。

  1. 了不了解线程安全?线程安全的本质什么?

多个线程访问共同的资源时,在某⼀个线程对资源进⾏写操作的中
途,其他线程对这个写了⼀半的资源进⾏了读操作/写操作,从⽽导致出
现数据错误。

4.如何实现线程安全

一.保持可见性的操作
1、使⽤ final 关键字。
2、使⽤ volatile 关键字。
3、加锁,锁释放时会强制将缓存刷新到主内存。
二.让操作原⼦性
1、加锁,保证操作的互斥性。
2、使⽤ CAS 指令(如 Unsafe compareAndSwapInt)。
3、使⽤原⼦数值类型(如 AtomicInteger)。
4、使⽤原⼦属性更新器(AtomicReferenceFieldUpdater)
。
三.禁⽌重排序
volatile 禁⽌重排序解决了 1.5 之前 DCL 写法因重排序导致的实
例没有完全初始化就被其它线程引⽤的情况
  1. 为什么多线程同时访问(读写)同个变量,会有并发问题?
1、Java 内存模型规定了所有的变量都存储在主内存中,⽽每个线程都
有⾃⼰的⼯作内存,线程的⼯作内存中保存了该线程中使⽤变量的主内
存副本拷⻉,线程对变量的所有操作都必须在⼯作内存中进⾏,⽽不能
直接读写主内存。
2、当线程访问⼀个变量时,会将变量从主内存拷⻉到⼯作内存,⽽对变
量的写操作,不会⻢上同步到主内存。
3、不同的线程之间也⽆法直接访问对⽅⼯作内存中的变量,线程间变量
的传递均需要在⾃⼰的⼯作内存和主存之间进⾏数据同步。

5.如何停止线程

终⽌线程的三种⽅式
1、使⽤stop⽅法强⾏终⽌线程,不安全主要有两点原因:
thread.stop()调⽤之后,创建⼦线程的线程就会抛出
ThreadDeathError的错误,并且会释放⼦线程持有的所有锁。
⼀个线程被 stop 停⽌并释放锁,其写⼊的内存数据写到⼀半没
有被清除,但此时另⼀个线程得到锁发现此内存区块的数据是异
常的。
2、使⽤interrupt()⽅法中断线程,但是线程不⼀定会终⽌。
Thread::Interrupted() 相⽐ Thread::IsInterrupted() ⽅法在
native底层仅仅多调⽤了⼀⾏ SetInterruptedLocked(false) 来清
空当前中断状态,其内部使⽤了 wait_mutex_ 互斥加锁来保证
线程安全。
3、使⽤volatile boolean变量退出标志,使线程正常退出,也就是当
run⽅法完成后线程终⽌。
interrupt是系统⽅法,并且使⽤了JNI调⽤、内部实现采⽤了加
锁,并且它的触发⽅式是采取了抛异常的⽅式,所有建议需要⽀
持系统⽅法时⽤中断,其它情况⽤volatile boolean标志位
  1. OKHttp源码有了解过吗,了解过讲下有价值的或者说可借鉴的设计思想,讲下OKhttp针对⽹络层有哪些优化
使⽤责任链模式实现拦截器的分层设计,每⼀个拦截器对应⼀个功能,
充分实现了功能解耦、易维护。
1、多路复⽤(ConnectInterceptor):在 StreamAllocation.newStream
⽅法中获取 RealConnection,在获取的时候我们会判断有没有之前的连
接可以复⽤。
2、缓存(CacheInterceptor): okhttp 的缓存策略是,key 为请求 url
的 MD5 值,value 为响应。
3、压缩(bridgeInterceptor): response 通过 bridgeInterceptor 处理
的时候会进⾏ gzip 压缩,这样可以⼤⼤减⼩我们的 response,但不是什
么情况下都压缩,只有⽀持的时候才会进⾏压缩。

能讲到责任链即可

  1. Retrofit源码有了解过吗,讲一下声明的interface到具体的⽣成⽹络请求对象的过程
1、创建retrofit实例,定义⼀个⽹络请求接⼝并为接⼝中的⽅法添加注
解。
2、通过动态代理⽣成⽹络请求对象:
通过 Retrofit.create(Class) 创建出 Service interface 的实例,从⽽使
得 Service 中配置的⽅法变得可⽤,这是 Retrofit 代码结构的核⼼,
Retrofit.create() 内部使⽤了 Proxy.newProxyInstance() 来创建
Service 实例以通过在运⾏时访问代理对象的⽅式来间接访问⽬标对
象。
这个⽅法会为参数中的多个 interface(具体到 Retrofit 来说,是
固定传⼊了⼀个 interface)去创建对象,这个对象实现了所有
interface 的每个⽅法,并且每个⽅法的实现都是雷同的:调⽤
对象实例内部的⼀个 InvocationHandler 成员变量的 invoke() ⽅
法,并把⾃⼰的⽅法信息传递进去。
这样就在实质上实现了代理逻辑:interface 中的⽅法全部由⼀
个另外设定的 InvocationHandler 对象来进⾏代理操作。并且,
这些⽅法的具体实现是运⾏时⽣成 interface 实例时才确定的,
⽽不是在编译时。
3、通过⽹络请求适配器将⽹络请求对象进⾏平台适配。
4、通过⽹络请求执⾏器发送⽹络请求。
这个过程调⽤loadServiceMethod(method)构建了⼀个
ServiceMethod对象,根据⽅法注解获取请求⽅式,参数类型和参数
注解拼接请求的连接。当⼀切都准备好之后会把数据添加到Retrofit
的RequestBuilder中,然后当我们主动发起⽹络请求的时候会调⽤
okhttp发起⽹络请求,okhttp的配置包括请求⽅式,URL等在
Retrofit的RequestBuilder的build()⽅法中实现,当调⽤
Call.enqueue() 时会利⽤ RequestFactory 和 OkHttpClient 来创建⼀
个 对象,并调⽤这个 对象来进⾏⽹络请
求的发起。
okhttp3.Call okhttp3.Call
5、通过数据转换器ResponseConverter解析数据。
在这⾥如果有⾃定义的 CallAdapter,这⾥也可以⽣成别的类型的对
象,例如 RxJava 的 Observable ,来让 Retrofit 可以和 RxJava 结合
使⽤。
6、通过回调执⾏器ExecutorCallbackCall把操作切回主线程后再交还给
Retrofit 的 callback 。
7、⽤户在主线程处理返回结果。

能讲到动态代理即可

  1. 讲下你熟悉的设计模式,Android中哪些部分对应使用了哪些设计模式(礼貌题 来面试的啥也答不上可以问问)
1. Okhttp内部使⽤了责任链模式
2、istView/RecyclerView的Adapter的notifyDataSetChanged⽅法、⼴播、
事件总线机制
3、ListView/RecyclerView/GridView的适配器模式
4、Context/ContextImpl外观模式/⻔⾯模式
  1. 什么是UI线程?
UI线程就是刷新UI所在的线程,并且它是单线程刷新的。
对Activity来说,UI线程就是主线程!
Activity.runOnUiThread(Runnable)
对View来说,它的UI线程就是ViewRootImpl创建的时候所在的线程!
  1. 谈你对Handler的理解?

Looper负责循环、条件判断和任务执⾏。Handler负责任务的定制和线
程间传递。MessageQueue是⼀个由单链表构成的优先级队列(取的都
是头部,所以说是队列)。
⼀个线程对应⼀个Looper,⼀个Looper有⼀个MessageQueue,⼀个
MessageQueue对应多个message,并且每⼀个message都持有对应的
handler,所以⼀个MessageQueue对应多个Handler。
要强调⼀下Message中的参数target(Handler),正是这个变量,每个
Message才能找到对应的Handler进⾏消息分发,让多个Handler同时⼯
作。

  1. 讲一下同步屏障?

⼀般情况下,同步和异步消息处理起来没有什么不同,只有在设置了
同步屏障之后才会有差异。同步屏障从代码层⾯上看是⼀个Message对
象,但是其target属性为null,⽤以区分普通消息。在
()中会判断如果队头消息是⼀个同步屏障消息,则跳过后⾯所有的同
步消息,找到第⼀个异步消息来处理。(如果不存在同步屏障消息,则
按时间顺序出队)
MessageQueue.ne
xt
⽽所有消息默认都是同步消息,只有⼿动设置了异步标志,这个消息才
会是异步消息。另外,同步屏障消息只能由内部来发送,这个接⼝并没
有公开给我们使⽤。所以,我们只能通过反射调⽤MessageQueue的
postSycBarrier/removeSyncBarrier去添加/删除⼀个同步屏障。
postSyncBarrier、sendMessageDelay、postDelay中使⽤uptimeMillis⽽
不使⽤currentTimeMillis计算时间的原因?
currentTimeMillis是⽤来获取系统当前时间,它可以被系统设置修
改,如果设置系统时间,时间值可能会发送变化。
uptimeMillis是⽤来获取⾃开机后经过的时间,它不包括深度休眠的
时间。
postSyncBarrier、sendMessageDelay、postDelay中使⽤
uptimeMillis能在发送了这条消息后,如果在这期间设备进⼊了休眠
状态,那么消息是不会执⾏的,设备唤醒之后才会执⾏。
⼀般异步消息和同步屏障消息会⼀同使⽤,异步消息 & 同步屏障的使⽤
场景
1、ViewRootImpl接收屏幕垂直同步信息事件⽤于驱动UI测绘流程:
Choreographer ⾥所有跟 message 有关的代码,你会发现,都
⼿动设置了异步消息的标志,所以这些操作是不受到同步屏障影
响的。这样做的原因可能就是为了尽可能保证上层 app 在接收
到屏幕刷新信号时,可以在第⼀时间执⾏遍历绘制 View 树的⼯
作。
同步屏障是在 scheduleTraversals() 被调⽤时才发送到消息队列
⾥的,也就是说,只有当某个 View 发起了刷新请求时,在这个
时刻后⾯的同步消息才会被拦截掉。如果在
scheduleTraversals() 之前就发送到消息队列⾥的⼯作仍然会按
顺序依次被取出来执⾏。
2、ActivityThread接收AMS的事件驱动⽣命周期。
3、InputMethodManager分发软键盘输⼊事件。
4、PhoneWindowManager分发电话⻚⾯各种事件

  1. IdleHandler 是什么?怎么用?

在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。
常见应用场景:Activity启动优化(加快App启动速度):onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间

  1. intent传大图,但是容易抛异常,原因是什么?

当parcelSize > 200kb时会抛出TransactionTooLargeException异常

  1. 跨进程传⼤图,有哪些⽅案

1、图⽚写到⽂件,路径传到另⼀个进程,再读出来。
2、通过IPC的⽅式转发图⽚数据:具体可以采⽤ binder + 匿名共享内存
的形式,像跨进程传递⼤的 bitmap 需要打开系统底层的 ashmem 机
制。(Bundle#putBinder)
常规的intent传递数据,在startActivity时会调⽤setAllowFds ⽅法将
allowFds 设置为 false, 然后就会将 Bitmap 直接写到 Parcel 缓冲
区,太⼤就出问题了。如果通过 bundle.putBinder形式传递
Bitmap,会在native层的writeBlob⽅法⾥⾯判断如果图⽚⼤于16k
,将会开辟⼀个匿名共享内存来存Bitmap的数据,⽽由于allowFds
此时为true,所以Parcel 缓冲区只是存储 FD 。
使⽤AIDL通过Binder进⾏IPC调⽤来传递图⽚。

  1. 运⾏时如何获取 Bitmap 的⼤⼩,具体的计算方式?

Bitmap占⽤内存= 宽 ⼀像素所占⽤字节内存(默认
Bitmap.Config.ARGB_8888为4字节,Bitmap.Config.ARGB_565为2
字节)。

  1. 进程传递⼤内存数据如何做?

一次Binder通信最⼤可以传输的数据:
如果是正常情况下的ProcessState初始化的binder服务,在oneway的情
况下为(1M-8K)/2,⾮oneway的情况为1M-8K。如果是⼿写mmap初始化
Binder服务,在oneway的情况下为2M,⾮oneway的情况4M。
所以直接使⽤binder肯定是不⾏的,因为正常情况下映射的最⼤内存只有
1M-8K,可以采⽤ binder + 匿名共享内存的形式,像跨进程传递⼤的 bitmap
需要打开系统底层的 ashmem机制

  1. SharedPrefrences的apply和commit有什么区别?

1、apply没有返回值,⽽commit返回boolean表明修改提交成功。
2、apply是将修改数据原⼦提交到内存,⽽后异步真正提交到硬件磁盘,⽽
commit则是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时
候,他们会等待正在处理的commit保存到磁盘后再操作,从⽽降低了效
率。⽽apply只是原⼦的提交到内容,后⾯有调⽤apply函数的将会直接覆盖
前⾯的内存数据,这样从⼀定程度上提⾼了很多效率。
3、apply⽅法不会有任何失败的提示。 由于在⼀个进程中,
sharedPreference是单实例,⼀般不会出现并发冲突,如果对提交的结果不
关⼼的话,建议使⽤apply,当然需要确保提交成功且有后续操作的话,还
是需要⽤commit。

  1. 如何解决 SharedPreference apply 引起的 ANR 问题?

apply为什么会引起ANR?
SP 调⽤ apply ⽅法,会创建⼀个等待锁放到 QueuedWork 中,并将真正
的数据持久化封装成⼀个任务放到异步队列中执⾏,任务执⾏结束会释
放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand
时,会执⾏ QueuedWork.waitToFinish() 等待所有的等待锁释放。
如何解决?
所有此类 ANR 都是经过 QueuedWork.waitToFinish() 触发的,只要在调
⽤此函数之前,将其中保存的队列⼿动清空即可。具体是通过Hook
ActivityThread的Handler实例,拿到此实例后给其设置⼀个Callback,
Handler 的 dispatchMessage 中会优先处理这个 Callback,最后在
Callback 中调⽤队列的清理⼯作

  1. 熟不熟悉自定义View,讲一下View树的测量流程

⾸先,在ViewGroup中的measureChildren()⽅法中会遍历测
量ViewGroup中所有的View,当View的可⻅性处于GONE状
态时,不会对其进⾏测量。
然后,测量某个指定的View时,根据⽗容器的MeasureSpec
和⼦View的LayoutParams等信息计算⼦View的MeasureSpec。
最后,将计算出的MeasureSpec传⼊View的measure⽅法,
⽽对于ViewGroup的测量,⼀般要重写onMeasure⽅法,在
onMeasure⽅法中,⽗容器会对所有的⼦View进⾏
Measure,⼦元素⼜会作为⽗容器,重复对它⾃⼰的⼦元素
进⾏Measure,这样Measure过程就从DecorView⼀级⼀级传
递下去了,也就是要遍历所有⼦View的的尺⼨,最终得出总
的ViewGroup的尺⼨。Layout和Draw⽅法也是如此。
(setMeasureDimension⽅法⽤于设置View的测量宽⾼,如
果View没有重写onMeasure⽅法,则会默认调⽤
getDefaultSize来获得View的宽⾼

对于⾃定义的单⼀view,⼀般可以不处理onMeasure⽅
法,如果要对宽⾼进⾏⾃定义,就重写onMeasure⽅
法,并将计算好的宽⾼通过setMeasuredDimension⽅法
传进去。对于⾃定义的ViewGroup,⼀般需要重写
onMeasure⽅法,并且调⽤measureChildren⽅法遍历所
有⼦View并进⾏测量(measureChild⽅法是测量具体某
⼀个view的宽⾼),然后可以通过
getMeasuredWidth/getMeasuredHeight获取宽⾼,最后
通过setMeasuredDimension⽅法存储本身的总宽⾼。

  1. 在⼦线程中更新 UI 不报错有哪⼏种⽅式?

1、主线程申请成功后然后在⼦线程申请
先在主线程中触发绘制,等到⼦线程中就不会触发重绘,因为View
为了防⽌重绘使⽤ PFLAG_FORCE_LAYOUT 标志位做了优化

2、在⼦线程中创建 ViewRootImpl

  1. 在硬件加速的⽀持下,如果控件只是经常 invalidate() ,⽽没有触发
    requestLayout() 是不会触发 ViewRootImpl.checkThread() 的
  2. 使用SurfaceView 或者 TextureView