一丶设计模式与使用场景
建造者模式:
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
使用场景比如最常见的 AlertDialog,拿我们开发过程中举例,比如 Camera 开发过 程中,可能需要设置一个初始化的相机配置,设置摄像头方向,闪光灯开闭,成 像质量等等,这种场景下就可以使用建造者模式
装饰者模式:
动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模 式比生成子类更为灵活。装饰者模式可以在不改变原有类结构的情况下曾强类的 功能,比如 Java 中的 BufferedInputStream
包装 FileInputStream
,举个开发中的 例子,比如在我们现有网络框架上需要增加新的功能,那么再包装一层即可,装 饰者模式解决了继承存在的一些问题,比如多层继承代码的臃肿,使代码逻辑更 清晰
还有等等.......观察者模式, 代理模式,门面模式,单例模式,生产者消费者模式。
二丶java 中的线程创建方式,线程池的工作原理
java 中有三种创建线程的方式,或者说四种
1.继承 Thread 类实现多线程
2.实现Runnable
接口
3.实现Callable
接口
4.通过线程池
线程池的工作原理: 线程池可以减少创建和销毁线程的次数,从而减少系统资源 的消耗,当一个任务提交到线程池时
a. 首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线 程执行任务,否则进入下一步
b. 判断工作队列是否已满,没有满则加入工作队列,否则执行下一步
c. 判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否 则执行饱和策略,默认抛出异常
三丶handler 原理
Handler
,Message
,looper
和 MessageQueue
构成了安卓的消息机制,handler 创 建后可以通过 sendMessage
将消息加入消息队列,然后 looper
不断的将消息从 MessageQueue
中取出来,回调到 Hander
的 handleMessage
方法,从而实现线程 的通信。
从两种情况来说,第一在 UI 线程创建 Handler,此时我们不需要手动开启 looper
, 因为在应用启动时,在 ActivityThread
的 main 方法中就创建了一个当前主线程的 looper
,并开启了消息队列,消息队列是一个无限循环,为什么无限循环不会 ANR? 因为可以说,应用的整个生命周期就是运行在这个消息循环中的,安卓是由事件 驱动的,Looper.loop
不断的接收处理事件,每一个点击触摸或者 Activity 每一个 生命周期都是在 Looper.loop
的控制之下的,looper.loop
一旦结束,应用程序的 生命周期也就结束了。我们可以想想什么情况下会发生 ANR,第一,事件没有得 到处理,第二,事件正在处理,但是没有及时完成,而对事件进行处理的就是looper
,所以只能说事件的处理如果阻塞会导致 ANR,而不能说 looper
的无限循 环会 ANR
另一种情况就是在子线程创建Handler,此时由于这个线程中没有默认开启的消息 队列,所以我们需要手动调用 looper.prepare()
,并通过 looper.loop
开启消息 主线程 Looper
从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程 往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读 取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此 loop 的循环并不会对 CPU 性能有过多的消耗。
四丶内存泄漏的场景和解决办法
1.非静态内部类的静态实例
非静态内部类会持有外部类的引用,如果非静态内部类的实例是静态的,就会长 期的维持着外部类的引用,组织被系统回收,解决办法是使用静态内部类
2.多线程相关的匿名内部类和非静态内部类
匿名内部类同样会持有外部类的引用,如果在线程中执行耗时操作就有可能发生 内存泄漏,导致外部类无法被回收,直到耗时任务结束,解决办法是在页面退出 时结束线程中的任务
3.Handler 内存泄漏
Handler 导致的内存泄漏也可以被归纳为非静态内部类导致的,Handler 内部 message 是被存储在 MessageQueue
中的,有些 message 不能马上被处理,存在 的时间会很长,导致 handler 无法被回收,如果 handler 是非静态的,就会导致 它的外部类无法被回收,
解决办法是 :
1.使用静态 handler,外部类引用使用弱引 用处理
2.在退出页面时移除消息队列中的消息
4.Context 导致内存泄漏
根据场景确定使用 Activity 的 Context 还是 Application
的 Context
,因为二者生命周 期不同,对于不必须使用 Activity 的 Context 的场景(Dialog),一律采用 Application
的 Context
,单例模式是最常见的发生此泄漏的场景,比如传入一个 Activity 的 Context 被静态类引用,导致无法回收.
5.静态 View 导致泄漏
使用静态 View 可以避免每次启动 Activity 都去读取并渲染 View,但是静态 View会持有 Activity 的引用,导致无法回收,解决办法是在 Activity 销毁的时候将静态 View 设置为 null(View 一旦被加载到界面中将会持有一个 Context 对象的引用, 在这个例子中,这个 context 对象是我们的 Activity,声明一个静态变量引用这个 View,也就引用了 activity)
6.WebView 导致的内存泄漏
WebView
只要使用一次,内存就不会被释放,所以 WebView
都存在内存泄漏的 问题,通常的解决办法是为 WebView
单开一个进程,使用 AIDL
进行通信,根据 业务需求在合适的时机释放掉
7.资源对象未关闭导致
如 Cursor,File 等,内部往往都使用了缓冲,会造成内存泄漏,一定要确保关闭 它并将引用置为 null
8.集合中的对象未清理
集合用于保存对象,如果集合越来越大,不进行合理的清理,尤其是入股集合是 静态的
9.Bitmap 导致内存泄漏
bitmap 是比较占内存的,所以一定要在不使用的时候及时进行清理,避免静态 变量持有大的 bitmap 对象
10.监听器未关闭
很多需要 register
和 unregister
的系统服务要在合适的时候进行 unregister
,手动添 加的 listener
也需要及时移除
五丶如何避免 OOM?
1.使用更加轻量的数据结构: 如使用 ArrayMap/SparseArray
替代 HashMap
,HashMap
更耗内存,因为它需要额外的实例对象来记录 Mapping
操作, SparseArray
更加高效,因为它避免了 Key Value
的自动装箱,和装箱后的解箱操 作
2.便面枚举的使用,可以用静态常量或者注解@IntDef 替代
3.Bitmap 优化:
a.尺寸压缩: 通过 InSampleSize
设置合适的缩放
b.颜色质量: 设置合适的 format,ARGB_6666/RBG_545/ARGB_4444/ALPHA_6,存在很大差异.
c.inBitmap: 使用 inBitmap
属性可以告知 Bitmap
解码器去尝试使用已经存在的内 存区域,新解码的 Bitmap
会尝试去使用之前那张 Bitmap 在 Heap 中所占据的 pixel data 内存区域,而不是去问内存重新申请一块区域来存放 Bitmap。利用这种特 性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的 内存大小,但复用存在一些限制,具体体现在:在 Android 4.4 之前只能重用相 同大小的 Bitmap 的内存,而 Android 4.4 及以后版本则只要后来的 Bitmap 比之前 的小即可。使用 inBitmap
参数前,每创建一个 Bitmap 对象都会分配一块内存供 其使用,而使用了 inBitmap
参数后,多个 Bitmap 可以复用一块内存,这样可以 提高性能
4.StringBuilder
替代 String: 在有些时候,代码中会需要使用到大量的字符串拼接 的操作,这种时候有必要考虑使用 StringBuilder
来替代频繁的“+”
5.避免在类似 onDraw
这样的方法中创建对象,因为它会迅速占用大量内存,引 起频繁的 GC 甚至内存抖动
6.减少内存泄漏也是一种避免 OOM 的方法
六丶onRestart
的调用场景
(1)按下 home 键之后,然后切换回来,会调用
onRestart()
。
(2)从本 Activity 跳转到另一个 Activity 之后,按 back 键返回原来 Activity,会 调用onRestart()
;
(3)从本 Activity 切换到其他的应用,然后再从其他应用切换回来,会调用onRestart()
;
说下 Activity 的横竖屏的切换的生命周期,用那个方法来保存数据,两者的区别。 触发在什么时候在那个方法里可以获取数据等。
七丶实现进程保活
a: Service 设置成 START_STICKY kill 后会被重启(等待 5 秒左右),重传 Intent,保 持与重启前一样
b: 通过startForeground
将进程设置为前台进程, 做前台服务,优先级和前台 应用一个级别,除非在系统内存非常缺,否则此进程不会被 kill
c: 双进程 Service: 让 2 个进程互相保护对方,其中一个 Service 被清理后,另 外没被清理的进程可以立即重启进程
d: 用 C 编写守护进程(即子进程) : Android 系统中当前进程(Process)fork 出来的子 进程,被系统认为是两个不同的进程。当父进程被杀死的时候,子进程仍然可以 存活,并不受影响(Android5.0 以上的版本不可行)联系厂商,加入白名单
e. 锁屏状态下,开启一个一像素 Activity
八丶ANR 的原因
1.耗时的网络访问
2.大量的数据读写
3.数据库操作
4.硬件操作(比如 camera)
5.调用 thread 的join()
方法、sleep()
方法、wait()
方法或者等待线程锁的时候
6.service binder 的数量达到上限
7.system server 中发生WatchDog ANR
8.service 忙导致超时无响应
9.其他线程持有锁,导致主线程等待超时
10.其它线程终止或崩溃导致主线程一直等待
九丶三级缓存原理
当 Android 端需要获得数据时比如获取网络中的图片,首先从内存中查找(按键 查找),内存中没有的再从磁盘文件或 sqlite
中去查找,若磁盘中也没有才通过 网络获取
十丶线程
1. 什么是线程
线程就是进程中运行的多个子任务,是操作系统调用的最小单元
2. 线程的状态
New: 新建状态,new 出来,还没有调用 start
Runnable: 可运行状态,调用 start 进入可运行状态,可能运行也可能没有运行, 取决于操作系统的调度
Blocked: 阻塞状态,被锁阻塞,暂时不活动,阻塞状态是线程阻塞在进入synchronized 关键字修饰的方法或代码块(获取锁)时的状态。
Waiting: 等待状态,不活动,不运行任何代码,等待线程调度器调度,wait sleep
Timed Waiting: 超时等待,在指定时间自行返回
Terminated: 终止状态,包括正常终止和异常终止
3. 线程的创建
a.继承 Thread 重写 run 方法
b.实现 Runnable
重写 run 方法
c.实现 Callable 重写 call 方法 实现 Callable 和实现 Runnable
类似,但是功能更强大,具体表现在
- 可以在任务结束后提供一个返回值,
Runnable
不行 - call 方法可以抛出异常,
Runnable
的 run 方法不行 - 可以通过运行 Callable 得到的
Fulture
对象监听目标线程调用 call 方法的结果, 得到返回值,(fulture.get()
,调用后会阻塞,直到获取到返回值)
4. 线程中断
一般情况下,线程不执行完任务不会退出,但是在有些场景下,我们需要手动控 制线程中断结束任务,Java 中有提供线程中断机制相关的 Api,每个线程都一个状 态位用于标识当前线程对象是否是中断状态
public boolean isInterrupted() //
判断中断标识位是否是 true,不会改变标 识位 public void interrupt() //
将中断标识位设置为 truepublic static boolean interrupted() //
判断当前线程是否被中断,并且该方法调用结束的时 候会清空中断标识位
需要注意的是 interrupt()
方法并不会真的中断线程,它只是将中断标识位设置 为 true,具体是否要中断由程序来判断,如下,只要线程中断标识位为 false,也就 是没有中断就一直执行线程方法
new Thread(new Runnable(){
while(!Thread.currentThread().isInterrupted()){
//执行线程方法
} }).start();
前边我们提到了线程的六种状态,New Runnable Blocked Waiting Timed Waiting Terminated
,那么在这六种状态下调用线程中断的代码会怎样呢,New 和 Terminated 状态下,线程不会理会线程中断的请求,既不会设置标记位,在 Runnable
和 Blocked 状态下调用 interrupt 会将标志位设置位 true,在 Waiting 和 Timed Waiting 状态下会发生 InterruptedException
异常,针对这个异常我们如何 处理?
1.在 catch 语句中通过 interrupt
设置中断状态,因为发生中断异常时,中断标志 位会被复位,我们需要重新将中断标志位设置为 true,这样外界可以通过这个状 态判断是否需要中断线程
try{
....
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
2.更好的做法是,不捕获异常,直接抛出给调用者处理,这样更灵活
5.Thread 为什么不能用 stop 方法停止线程
从 SUN 的官方文档可以得知,调用 Thread.stop()
方法是不安全的,这是因为当调 用 Thread.stop()
方法时,会发生下面两件事:
- 即刻抛出
ThreadDeath
异常,在线程的run()
方法内,任何一点都有可能抛出ThreadDeath Error
,包括在catch
或finally
语句中。 - 释放该线程所持有的所有的锁。调用 thread.stop()后导致了该线程所持有的所有锁的 突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时, 有可能导致一些很奇怪的应用程序错误。
6.进程线程的区别
1.地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地 址空间。
2.资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu 等,但是进 程之间的资源是独立的。
3.一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃 整个进程都死掉。所以多进程要比多线程健壮。
4.进程切换时,消耗的资源大,效率不高。所以涉及到频繁的切换时,使用线程 要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用 线程不能用进程
5.执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入 口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程 执行控制。
6.线程是处理器调度的基本单位,但是进程不是。
7.两者均可并发执行。
未完待续......
更多内容的面试汇总PDF版本,含有BATJ.字节跳动面试专题,算法专题,高端技术专题,混合开发专题,java面试专题,Android,Java小知识,到性能优化.线程.View.OpenCV.NDK。