本周参加上海某公司的 Android 的面试,遇到了很多问题,下面我把最经典的题目列举出来,并提供这些问题的答案,希望对大家有帮助。

内存溢出的问题你是怎么解决的?

我的回答:
一般不会发生 OOM,当我们在 ListView 中加载大图的时候并且上下快速滑动的时候回产生 OOM 的问题。为了解决这个问题,我废了很大的脑筋。我从网上找了一些缓存策略,网上说缓存分为网络缓存,内存缓存,本地缓存,图片从网上下载完了保存在本地,下次从本地去读取,读完了往内存里放一份,内存缓存这一块儿,我最先考虑的是用哈希 MAP,但是我会发现一个问题,当 ListView 加载的图片一多,这个哈希 MAP 会越来越大,最终导致内存溢出。这个很麻烦,但是我又没有想到合适的解决方法。接着去网上搜索解决哈希 MAP 过大的资料,网上说哈希 MAP 太大,垃圾回收器怎么也不回收,是因为哈希 MAP 属于强引用。然后我就了解了一下 JVM 的强引用,软引用,弱引用,虚引用的差别。后来了解到要想 Bitmap 资源及时回收,得搞成一个软引用,我就把原来的Bitmap 包装了一下,效果还不错,当时测试的是 2.3 以下的模拟器,后来换到4.0模拟器,发现好像没什么效果了。绝大部分的图片还是从本地缓存读取,内存缓存跟没有一样。网上明明说的是软引用靠谱,但是我感觉不靠谱,后来查了一下谷歌官方文档,在谷歌的一篇博客里面明确的说到2.3以后的版本,Android 将倾向于优先回收软引用、弱引用、虚引用。同时 google 官方推出了一个叫做 Lrucache 的解决方案。后来我就用了 LruCache,发现在 4.0 的系统里表现很很不错,用法和哈希 MAP差不多,只不过需要在实例化的时候指定最大内存,最大内存一般指定的是 16M 最大内存的 1/8, 这个 1/8 也是网上主流的解决方案。但是我很好奇,LruCAche 怎么做到的,后来看了一下他的源代码,发现原理非常的简单,他底层就是一个 LinkedHashMap,就是不断判断当前内存是否超过设定的最大内存,超过了就从 LinkedHashMap 尾部移除一个元素。自己写一个 LruCache 其实也很容易。后来又发现某些小图片没必要加载那么清晰,我就对部分图片做了压缩处理(设置清晰度,缩放比),这样效果就非常好了,在 2.3 之前和之后的模拟器上都表现良好。

ListView 的优化,你是怎么做的?

分析过程

  • 如果item中有图片一定要用异步加载。而且里面的图片尽量要用缩略图或者小图。
  • 判断手势,快速滑动时不加载里面的图片。
  • 要对数据进行分页加载。
  • item 的布局层级要越少越好。

检查过程

使用 Hierarchy Viewer 检查布局:
Hierarchy Viewer 是随 Android SDK 发布的工具,位于 Android SDK/tools/hierarchyviewer.bat (Windows 操作系统,mac 上显示的为hierarchyviewer),使用起来也是超级简单,通过此工具可以详细的理解当前界面的控件布局以及某个控件的属性(name、id、height等)。

  • 连接设备真机或者模拟器。
  • 启动你要观察的应用。
  • 打开 Hierarchyviewer,点击 hierarchyviewer 文件即可。

请回答 Handler 的原理

andriod 提供了 Handler 和 Looper 来满足线程间的通信。Handler 先进先出原则。Looper 类用来管理特定线程内对象之间的消息交换( Message Exchange )。  

Looper: 一个线程可以产生一个 Looper 对象,由它来管理此线程里的Message Queue (消息队列)。  

Handler: 你可以构造 Handler 对象来与 Looper 沟通,以便 push 新消息到 Message Queue 里;或者接收 Looper 从 Message Queue 取出)所送来的消息。   

Message Queue (消息队列):用来存放线程放入的消息。  

线程:UI thread 通常就是 main thread,而 Android 启动程序时会替它建立一个Message Queue。

你是如何避免 anr 的问题

答:ANR:Application Not Responding,五秒在 Android 中,活动管理器和窗口管理器这两个系统服务负责监视应用程序的响应。当出现下列情况时,Android 就会显示 ANR 对话框了: 对输入事件(如按键、触摸屏事件)的响应超过5秒 意向接受器( intentReceiver )超过10秒钟仍未执行完毕。

Android 应用程序完全运行在一个独立的线程中(例如 main )。这就意味着,任何在主线程中运行的,需要消耗大量时间的操作都会引发 ANR。因为此时,你的应用程序已经没有机会去响应输入事件和意向广播( Intent broadcast )。   

因此,任何运行在主线程中的方法,都要尽可能的只做少量的工作。特别是活动生命周期中的重要方法如 onCreate() 和 onResume() 等更应如此。潜在的比较耗时的操作,如访问网络和数据库;或者是开销很大的计算,比如改变位图的大小,需要在一个单独的子线程中完成(或者是使用异步请求,如数据库操作)。但这并不意味着你的主线程需要进入阻塞状态已等待子线程结束 – 也不需要调用 Therad.wait() 或者 Thread.sleep() 方法。取而代之的是,主线程为子线程提供一个句柄(Handler),让子线程在即将结束的时候调用它(xing:可以参看 Snake 的例子,这种方法与以前我们所接触的有所不同)。使用这种方法涉及你的应用程序,能够保证你的程序对输入保持良好的响应,从而避免因为输入事件超过5秒钟不被处理而产生的 ANR。这种实践需要应用到所有显示用户界面的线程,因为他们都面临着同样的超时问题。

如何检测内存泄露

检测

1、DDMS Heap 发现内存泄露 dataObject totalSize 的大小,是否稳定在一个范围内,如果操作程序,不断增加,说明内存泄露

2、使用 Heap Tool 进行内存快照前后对比 BlankActivity 手动触发GC进行前后对比,对象是否被及时回收

定位

1、MAT插件打开.hprof具体定位内存泄露:
查看 histogram 项,选中某一个对象,查看它的 GC 引用链,因为存在GC 引用链的,说明无法回收

2、AndroidStudio 的 Allocation Tracker:
观测到期间的内存分配,哪些对象被创建,什么时候创建,从而准确定位。