Android 基础面试题(一)

1、什么是ANR 如何避免它?

答:在Android上,如果你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应 用程序无响应(ANR:Application NotResponding)对话框。 用户可以选择让程序继续运行,但是,他们在使用你的 应用程序时,并不希望每次都要处理这个对话框。因此 ,在程序里对响应性能的设计很重要这样,这样系统就不会显 示ANR给用户。

不同的组件发生ANR的时间不一样,Activity是5秒,BroadCastReceiver是10秒,Service是20秒(均为前台)。

如果开发机器上出现问题,我们可以通过查看/data/anr/traces.txt即可,最新的ANR信息在最开始部分。

  • 主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。
  • 主线程中存在耗时的计算
  • 主线程中错误的操作,比如Thread.wait或者Thread.sleep等 Android系统会监控程序的响应状况,一旦出现下面两种情况,则弹出ANR对话框
  • 应用在5秒内未响应用户的输入事件(如按键或者触摸)
  • BroadcastReceiver未在10秒内完成相关的处理
  • Service在特定的时间内无法处理完成 20秒

修正:

  1. 使用AsyncTask处理耗时IO操作。
  2. 使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同。
  3. 使用Handler处理工作线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程。
  4. Activity的onCreate和onResume回调中尽量避免耗时的代码。 BroadcastReceiver中onReceive代码也要尽量减少耗时,建议使用IntentService处理。
解决方案:

将所有耗时操作,比如访问网络,Socket通信,查询大 量SQL 语句,复杂逻辑计算等都放在子线程中去,然 后通过handler.sendMessage、runonUIThread、AsyncTask、RxJava等方式更新UI。无论如何都要确保用户界面的流畅 度。如果耗时操作需要让用户等待,那么可以在界面上显示度条。

ANR 详解

2、Activity和Fragment生命周期有哪些?

3、横竖屏切换时候Activity的生命周期

不设置Activity的android:configChanges时,切屏会重新回调各个生命周期,切横屏时会执行一次,切竖屏时会执行两次。 设置Activity的android:configChanges=”orientation”时,切屏还是会调用各个生命周期,切换横竖屏只会执行一次 设置Activity的android:configChanges=”orientation |keyboardHidden”时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

4、AsyncTask的缺陷和问题,说说他的原理。

AsyncTask是什么?

AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI。

AsyncTask是一个抽象的泛型类,它提供了Params、Progress和Result这三个泛型参数,其中Params表示参数的类型,Progress表示后台任务的执行进度和类型,而Result则表示后台任务的返回结果的类型,如果AsyncTask不需要传递具体的参数,那么这三个泛型参数可以用Void来代替。

关于线程池:

AsyncTask对应的线程池ThreadPoolExecutor都是进程范围内共享的,且都是static的,所以是Asynctask控制着进程范围内所有的子类实例。由于这个限制的存在,当使用默认线程池时,如果线程数超过线程池的最大容量,线程池就会爆掉(3.0后默认串行执行,不会出现个问题)。针对这种情况,可以尝试自定义线程池,配合Asynctask使用。

关于默认线程池:

AsyncTask里面线程池是一个核心线程数为CPU + 1,最大线程数为CPU * 2 + 1,工作队列长度为128的线程池,线程等待队列的最大等待数为28,但是可以自定义线程池。线程池是由AsyncTask来处理的,线程池允许tasks并行运行,需要注意的是并发情况下数据的一致性问题,新数据可能会被老数据覆盖掉。所以希望tasks能够串行运行的话,使用SERIAL_EXECUTOR。

AsyncTask在不同的SDK版本中的区别:

调用AsyncTask的execute方法不能立即执行程序的原因及改善方案通过查阅官方文档发现,AsyncTask首次引入时,异步任务是在一个独立的线程中顺序的执行,也就是说一次只执行一个任务,不能并行的执行,从1.6开始,AsyncTask引入了线程池,支持同时执行5个异步任务,也就是说只能有5个线程运行,超过的线程只能等待,等待前的线程直到某个执行完了才被调度和运行。换句话说,如果进程中的AsyncTask实例个数超过5个,那么假如前5都运行很长时间的话,那么第6个只能等待机会了。这是AsyncTask的一个限制,而且对于2.3以前的版本无法解决。如果你的应用需要大量的后台线程去执行任务,那么只能放弃使用AsyncTask,自己创建线程池来管理Thread。不得不说,虽然AsyncTask较Thread使用起来方便,但是它最多只能同时运行5个线程,这也大大局限了它的作用,你必须要小心设计你的应用,错开使用AsyncTask时间,尽力做到分时,或者保证数量不会大于5个,否就会遇到上面提到的问题。可能是Google意识到了AsynTask的局限性了,从Android 3.0开始对AsyncTask的API做出了一些调整:每次只启动一个线程执行一个任务,完了之后再执行第二个任务,也就是相当于只有一个后台线程在执行所提交的任务。

一些问题:
  1. 生命周期:很多开发者会认为一个在Activity中创建的AsyncTask会随着Activity的销毁而销毁。然而事实并非如此。AsynTask会一直执行,直到doInBackground()方法执行完毕,然后,如果cancel(boolean)被调用,那么onCancelled(Result result)方法会被执行;否则,执行onPostExecute(Result result)方法。如果我们的Activity销毁之前,没有取消AsyncTask,这有可能让我们的应用崩溃(crash)。因为它想要处理的view已经不存在了。所以,我们是必须确保在销毁活动之前取消任务。总之,我们使用AsyncTask需要确保AsyncTask正确的取消。
  2. 内存泄漏:如果AsyncTask被声明为Activity的非静态内部类,那么AsyncTask会保留一个对Activity的引用。如果Activity已经被销毁,AsyncTask的后台线程还在执行,它将继续在内存里保留这个引用,导致Activity无法被回收,引起内存泄漏。
  3. 结果丢失:屏幕旋转或Activity在后台被系统杀掉等情况会导致Activity的重新创建,之前运行的AsyncTask会持有一个之前Activity的引用,这个引用已经无效,这时调用onPostExecute()再去更新界面将不再生效。
  4. 并行还是串行:在Android1.6之前的版本,AsyncTask是串行的,在1.6之后的版本,采用线程池处理并行任务,但是从Android 3.0开始,为了避免AsyncTask所带来的并发错误,又采用一个线程来串行执行任务。可以使用executeOnExecutor()方法来并行地执行任务。
AsyncTask原理
  • AsyncTask中有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler),其中线程池SerialExecutor用于任务的排队,而线程池THREAD_POOL_EXECUTOR用于真正地执行任务,InternalHandler用于将执行环境从线程池切换到主线程。
  • sHandler是一个静态的Handler对象,为了能够将执行环境切换到主线程,这就要求sHandler这个对象必须在主线程创建。由于静态成员会在加载类的时候进行初始化,因此这就变相要求AsyncTask的类必须在主线程中加载,否则同一个进程中的AsyncTask都将无法正常工作。

5、onSaveInstanceState() 与 onRestoreIntanceState()

Activity的 onSaveInstanceState() 和 onRestoreInstanceState()并不是生命周期方法,它们不同于 onCreate()、onPause()等生命周期方法,它们并不一定会被触发。当应用遇到意外情况(如:内存不足、用户直接按Home键)由系统销毁一个Activity时,onSaveInstanceState() 会被调用。但是当用户主动去销毁一个Activity时,例如在应用中按返回键,onSaveInstanceState()就不会被调用。因为在这种情况下,用户的行为决定了不需要保存Activity的状态。通常onSaveInstanceState()只适合用于保存一些临时性的状态,而onPause()适合用于数据的持久化保存。 在activity被杀掉之前调用保存每个实例的状态,以保证该状态可以在onCreate(Bundle)或者onRestoreInstanceState(Bundle) (传入的Bundle参数是由onSaveInstanceState封装好的)中恢复。这个方法在一个activity被杀死前调用,当该activity在将来某个时刻回来时可以恢复其先前状态。 例如,如果activity B启用后位于activity A的前端,在某个时刻activity A因为系统回收资源的问题要被杀掉,A通过onSaveInstanceState将有机会保存其用户界面状态,使得将来用户返回到activity A时能通过onCreate(Bundle)或者onRestoreInstanceState(Bundle)恢复界面的状态

6、Android中进程的优先级?

  1. 前台进程:即与用户正在交互的Activity或者Activity用到的Service等,如果系统内存不足时前台进程是最晚被杀死的
  2. 可见进程:可以是处于暂停状态(onPause)的Activity或者绑定在其上的Service,即被用户可见,但由于失了焦点而不能与用户交互
  3. 服务进程:其中运行着使用startService方法启动的Service,虽然不被用户可见,但是却是用户关心的,例如用户正在非音乐界面听的音乐或者正在非下载页面下载的文件等;当系统要空间运行,前两者进程才会被终止
  4. 后台进程:其中运行着执行onStop方法而停止的程序,但是却不是用户当前关心的,例如后台挂着的QQ,这时的进程系统一旦没了有内存就首先被杀死
  5. 空进程:不包含任何应用程序的进程,这样的进程系统是一般不会让他存在的

7、Bunder传递对象为什么需要序列化?Serialzable和Parcelable的区别?

因为bundle传递数据时只支持基本数据类型,所以在传递对象时需要序列化转换成可存储或可传输的本质状态(字节流)。序列化后的对象可以在网络、IPC(比如启动另一个进程的Activity、Service和Reciver)之间进行传输,也可以存储到本地。

  • Serializable(Java自带):Serializable 是序列化的意思,表示将一个对象转换成存储或可传输的状态。序列化后的对象可以在网络上进传输,也可以存储到本地。
  • Parcelable(Android专用):除了Serializable之外,使用Parcelable也可以实现相同的效果,不过不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这也就实现传递对象的功能了。

8、动画

  • tween 补间动画。通过指定View的初末状态和变化方式,对View的内容完成一系列的图形变换来实现动画效果。 Alpha, Scale ,Translate, Rotate。
  • frame 帧动画。AnimationDrawable控制animation-list.xml布局
  • PropertyAnimation 属性动画3.0引入,属性动画核心思想是对值的变化。

Property Animation 动画有两个步聚:

  1. 计算属性值
  2. 为目标对象的属性设置属性值,即应用和刷新动画

计算属性分为3个过程:

  1. 过程一:计算已完成动画分数 elapsed fraction。为了执行一个动画,你需要创建一个ValueAnimator,并且指定目标对象属性的开始、结束和持续时间。在调用 start 后的整个动画过程中,ValueAnimator 会根据已经完成的动画时间计算得到一个0 到 1 之间的分数,代表该动画的已完成动画百分比。0表示 0%,1 表示 100%。
  2. 过程二:计算插值(动画变化率)interpolated fraction 。当 ValueAnimator计算完已完成的动画分数后,它会调用当前设置的TimeInterpolator,去计算得到一个interpolated(插值)分数,在计算过程中,已完成动画百分比会被加入到新的插值计算中。
  3. 过程三:计算属性值当插值分数计算完成后,ValueAnimator会根据插值分数调用合适的 TypeEvaluator去计算运动中的属性值。 以上分析引入了两个概念:已完成动画分数(elapsed fraction)、插值分数( interpolated fraction )。
原理及特点:
  1. 属性动画:
    1. 插值器:作用是根据时间流逝的百分比来计算属性变化的百分比
    2. 估值器:在1的基础上由这个东西来计算出属性到底变化了多少数值的类
    3. 其实就是利用插值器和估值器,来计出各个时刻View的属性,然后通过改变View的属性来实现View的动画效果。
  2. View动画:只是影像变化,view的实际位置还在原来地方。
  3. 帧动画:是在XML中定义好一系列图片之后,使用AnimatonDrawable来播放的动画。
它们的区别:

属性动画才是真正的实现了 view 的移动,补间动画对view 的移动更像是在不同地方绘制了一个影子,实际对象还是处于原来的地方。 当动画的 repeatCount 设置为无限循环时,如果在Activity退出时没有及时将动画停止,属性动画会导致Activity无法释放而导致内存泄漏,而补间动画却没问题。 XML 文件实现的补间动画,复用率极高。在 Activity切换,窗口弹出时等情景中有着很好的效果。 使用帧动画时需要注意,不要使用过多特别大的图,容导致内存不足。

为什么属性动画移动后仍可点击?

播放补间动画的时候,我们所看到的变化,都只是临时的。而属性动画呢,它所改变的东西,却会更新到这个View所对应的矩阵中,所以当ViewGroup分派事件的时候,会正确的将当前触摸坐标,转换成矩阵变化后的坐标,这就是为什么播放补间动画不会改变触摸区域的原因了。

9、Context相关

  1. Activity和Service以及Application的Context是不一样的,Activity继承自ContextThemeWraper.其他的继承自ContextWrapper。
  2. 每一个Activity和Service以及Application的Context是一个新的ContextImpl对象。
  3. getApplication()用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那也许在绝大多数情况下我们都是在Activity或者Servic中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法,getApplicationContext()比getApplication()方法的作用域会更广一些,任何一个Context的实例,只要调用getApplicationContext()方法都可以拿到我们的Application对象。
  4. 创建对话框时不可以用Application的context,只能用Activity的context。
  5. Context的数量等于Activity的个数 + Service的个数 +1,这个1为Application。

10、Android各版本新特性

Android5.0新特性
  • MaterialDesign设计风格
  • 支持64位ART虚拟机 (5.0推出的ART虚拟机,在5.0之前都是Dalvik。他们的区别是: Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。 ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT))
  • 通知详情可以用户自己设计
Android6.0新特性
  • 动态权限管理
  • 支持快速充电的切换
  • 支持文件夹拖拽应用
  • 相机新增专业模式
Android7.0新特性
  • 多窗口支持
  • V2签名
  • 增强的Java8语言模式
  • 夜间模式
Android8.0(O)新特性
  • 优化通知 通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除
  • 画中画模式 :清单中Activity设置android:supportsPictureInPicture
  • 后台限制
  • 自动填充框架
  • 系统优化
  • 等等优化很多
Android9.0(P)新特性
  • 室内WIFI定位
  • “刘海”屏幕支持
  • 安全增强
  • 等等优化很多
Android10.0(Q)目前曝光的新特性
  • 夜间模式 :包括手机上的所有应用都可以为其设置暗黑模式。
  • 桌面模式 :提供类似于PC的体验,但是远远不能代替PC。
  • 屏幕录制 :通过长按“电源”菜单中的"屏幕快照"来开启。

11、JSON

JSON的全称是JavaScript Object Notation,也就是JavaScript 对象表示法 JSON是存储和交换文本信息的语法,类似XML,但是比XML更小、更快,更易解析 JSON是轻量级的文本数据交换格式,独立于语言,具有可描述性,更易理解,对象可以包含多个名称/值对,比如:{"name":"zhangsan" , "age":25}

使用谷歌的GSON包进行解析,在 Android Studio 里引入依赖:compile 'com.google.code.gson:gson:2.7'

值得注意的是实体类中变量名称必须和JSON中的值名字相同。

使用示例:
  1. 解析成实体类:
    Gson gson = new Gson();
    Student student = gson.fromJson(json1, Student.class);
    
  2. 解析成int数组:
    Gson gson = new Gson();
    int[] ages = gson.fromJson(json2, int[].class);
    
  3. 直接解析成List:
    Gson gson = new Gson();
    List<Integer> ages = gson.fromJson(json2,  newTypeToken<List<Integer>>(){}.getType);
    Gson gson = new Gson();
    List<Student> students = gson.fromJson(json3, newTypeToke<List<Student>>(){}.getType);
    
优点:
  • 轻量级的数据交换格式
  • 读写更加容易
  • 易于机器的解析和生成
缺点:
  • 语义性较差,不如 XML 直观

12、Android中有哪几种解析XML的类,官方推荐哪种?以及它们的原理和区别?

DOM解析
  • 优点:
    • XML树在内存中完整存储,因此可以直接修改其数据结构.
    • 可以通过该解析器随时访问XML树中的任何一个节点.
    • DOM解析器的API在使用上也相对比较简单.
  • 缺点:
    • 如果XML文档体积比较大时,将文档读入内存是非消耗系统资源的.
  • 使用场景:
    • DOM 是与平台和语言无关的方式表示 XML文档的官方 W3C 标准.
    • DOM 是以层次结构组织的节点的集合.这个层次结构允许开人员在树中寻找特定信息.分析该结构通常需要加载整个文档和构造层次结构,然后才能进行任何工作.
    • DOM 是基于对象层次结构的.
SAX解析
  • 优点:
    • SAX 对内存的要求比较低,因为它让开发人员自己来决定所要处理的标签.特别是当开发人员只需要处理文档中包含的部分数据时,SAX 这种扩展能力得到了更好的体现.
  • 缺点:
    • 用SAX方式进行XML解析时,需要顺序执行,所以很难访问同一文档中的不同数据.此外,在基于该方式的解析编码程序也相对复杂.
  • 使用场景:
    • 对于含有数据量十分巨大,而又不用对文档的所有数据行遍历或者分析的时候,使用该方法十分有效.该方法不将整个文档读入内存,而只需读取到程序所需的文档标记处即可.
Xmlpull解析

Android SDK提供了xmlpullapi,xmlpull和sax类似,是基于流(stream)操作文件,后者根据节点事件回调开发者编写的处理程序.因为是基于流的处理,因此xmlpull和sax都比较节约内存资源,不会像dom那样要把所有节点以对象树的形式展现在内存中.xmpull比sax更简明,而且不需要扫描完整个流.

13、Jar和Aar的区别

Jar包里面只有代码,aar里面不光有代码还包括资源文件,比如 drawable 文件,XML资源文件。对于一些不常变动的 Android Library,我们可以直接引用 aar,加快编译速度。

14、Android为每个应用程序分配的内存大小是多少

Android程序内存一般限制在16M,也有的是24M。近几年手机发展较快,一般都会分配两百兆左右,和具体机型有关。

15、更新UI方式

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable),View.postDelay(Runnable, long)(可以理解为在当前操作视图UI线程添加队列)
  • Handler
  • AsyncTask
  • Rxjava
  • LiveData

16、ContentProvider使用方法。

进行跨进程通信,实现进程间的数据交互和共享。通过Context 中 getContentResolver() 获得实例,通过 Uri匹配进行数据的增删改查。ContentProvider使用表的形式来组织数据,无论数据的来源是什么,ConentProvider 都会认为是一种表,然后把数据组织成表格。

17、Thread、AsyncTask、IntentService的使用场景与特点。

  1. Thread线程,独立运行与于 Activity 的,当Activity 被 finish 后,如果没有主动停止 Thread或者 run 方法没有执行完,其会一直执行下去。
  2. AsyncTask 封装了两个线程池和一个Handler(SerialExecutor用于排队,THREAD_POOL_EXECUTOR为真正的执行任务,Handler将工作线程切换到主线程),其必须在 UI线程中创建,execute 方法必须在 UI线程中执行,一个任务实例只允许执行一次,执行多次抛出异常,用于网络请求或者简单数据处理。
  3. IntentService:处理异步请求,实现多线程,在onHandleIntent中处理耗时操作,多个耗时任务会依次执行,执行完毕自动结束。

18、Merge、ViewStub 的作用。

  • Merge: 减少视图层级,可以删除多余的层级。
  • ViewStub: 按需加载,减少内存使用量、加快渲染速度、不支持 merge 标签。

19、activity的startActivity和context的startActivity区别?

  • 从Activity中启动新的Activity时可以直接mContext.startActivity(intent)就好
  • 如果从其他Context中启动Activity则必须给intent设置Flag:
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ; 
    mContext.startActivity(intent);
    
20、怎么在Service中创建Dialog对话框?
  1. 在我们取得Dialog对象后,需给它设置类型,即:
    dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
    
  2. 在Manifest中加上权限:
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINOW" />
    

21、Asset目录与res目录的区别?

  • assets:不会在 R 文件中生成相应标记,存放到这里的资源在打包时会打包到程序安装包中。(通过 AssetManager 类访问这些文件)
  • res:会在 R 文件中生成 id 标记,资源在打包时如果使用到则打包到安装包中,未用到不会打入安装包中。
  • res/anim:存放动画资源。
  • res/raw:和 asset 下文件一样,打包时直接打入程序安装包中(会映射到 R 文件中)。

22、Android怎么加速启动Activity?

  • onCreate() 中不执行耗时操作 把页面显示的 View 细分一下,放在 AsyncTask 里逐步显示,用 Handler 更好。这样用户的看到的就是有层次有步骤的一个个的 View 的展示,不会是先看到一个黑屏,然后一下显示所有 View。最好做成动画,效果更自然。
  • 利用多线程的目的就是尽可能的减少 onCreate() 和 onReume() 的时间,使得用户能尽快看到页面,操作页面。
  • 减少主线程阻塞时间。
  • 提高 Adapter 和 AdapterView 的效率。
  • 优化布局文件。

23、Handler机制

Android消息循环流程图如下所示:

主要涉及的角色如下所示:

  • message:消息。
  • MessageQueue:消息队列,负责消息的存储与管理,负责管理由 Handler 发送过来的 Message。读取会自动删除消息,单链表维护,插入和删除上有优势。在其next()方法中会无限循环,不断判断是否有消息,有就返回这条消息并移除。
  • Looper:消息循环器,负责关联线程以及消息的分发,在该线程下从 MessageQueue获取 Message,分发给Handler,Looper创建的时候会创建一个 MessageQueue,调用loop()方法的时候消息循环开始,其中会不断调用messageQueue的next()方法,当有消息就处理,否则阻塞在messageQueue的next()方法中。当Looper的quit()被调用的时候会调用messageQueue的quit(),此时next()会返回null,然后loop()方法也就跟着退出。
  • Handler:消息处理器,负责发送并处理消息,面向开发者,提供 API,并隐藏背后实现的细节。

整个消息的循环流程还是比较清晰的,具体说来:

  1. Handler通过sendMessage()发送消息Message到消息队列MessageQueue。
  2. Looper通过loop()不断提取触发条件的Message,并将Message交给对应的target handler来处理。
  3. target handler调用自身的handleMessage()方法来处理Message。

事实上,在整个消息循环的流程中,并不只有Java层参与,很多重要的工作都是在C++层来完成的。我们来看下这些类的调用关系。

注:虚线表示关联关系,实线表示调用关系。

在这些类中MessageQueue是Java层与C++层维系的桥梁,MessageQueue与Looper相关功能都通过MessageQueue的Native方法来完成,而其他虚线连接的类只有关联关系,并没有直接调用的关系,它们发生关联的桥梁是MessageQueue。

总结
  • Handler 发送的消息由 MessageQueue 存储管理,并由 Looper 负责回调消息到 handleMessage()。
  • 线程的转换由 Looper 完成,handleMessage() 所在线程由 Looper.loop() 调用者所在线程决定。
Handler 引起的内存泄露原因以及最佳解决方案

Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 会泄露。 这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。

解决:将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。

为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?

通常我们认为 ActivityThread 就是主线程。事实上它并不是一个线程,而是主线程操作的管理者。在 ActivityThread.main() 方法中调用了 Looper.prepareMainLooper() 方法创建了 主线程的 Looper ,并且调用了 loop() 方法,所以我们就可以直接使用 Handler 了。

因此我们可以利用 Callback 这个拦截机制来拦截 Handler 的消息。如大部分插件化框架中Hook ActivityThread.mH 的处理。

主线程的 Looper 不允许退出

主线程不允许退出,退出就意味 APP 要挂。

Handler 里藏着的 Callback 能干什么?

Handler.Callback 有优先处理消息的权利 ,当一条消息被 Callback 处理并拦截(返回 true),那么 Handler 的 handleMessage(msg) 方法就不会被调用了;如果 Callback 处理了消息,但是并没有拦截,那么就意味着一个消息可以同时被 Callback 以及 Handler 处理。

创建 Message 实例的最佳方式

为了节省开销,Android 给 Message 设计了回收机制,所以我们在使用的时候尽量复用 Message ,减少内存消耗:

  • 通过 Message 的静态方法 Message.obtain();
  • 通过 Handler 的公有方法 handler.obtainMessage()。
子线程里弹 Toast 的正确姿势

本质上是因为 Toast 的实现依赖于 Handler,按子线程使用 Handler 的要求修改即可,同理的还有 Dialog。

妙用 Looper 机制
  • 将 Runnable post 到主线程执行;
  • 利用 Looper 判断当前线程是否是主线程。
主线程的死循环一直运行是不是特别消耗CPU资源呢?

并不是,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质是同步I/O,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

handler postDelay这个延迟是怎么实现的?

handler.postDelay并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。

24、程序A能否接收到程序B的广播?

能,使用全局的BroadCastRecevier能进行跨进程通信,但是注意它只能被动接收广播。此外,LocalBroadCastRecevier只限于本进程的广播间通信。

25、数据加载更多涉及到分页,你是怎么实现的?

分页加载就是一页一页加载数据,当滑动到底部、没有更多数据加载的时候,我们可以手动调用接口,重新刷新RecyclerView。

26、通过Google提供的Gson解析JSON时,定义JavaBean的规则是什么?

  1. 实现序列化 Serializable
  2. 属性私有化,并提供get,set方法
  3. 提供无参构造
  4. 属性名必须与JSON串中属性名保持一致 (因为Gson解析JSON串底层用到了Java的反射原理)

27、JSON解析方式的两种区别?

  1. SDK提供JSONArray,JSONObject
  2. Google提供的 Gson 通过fromJson()实现对象的反序列化(即将JSON串转换为对象类型) 通过toJson()实现对象的序列化 (即将对象类型转换为JSON串)

28、线程池的相关知识。

Android中的线程池都是直接或间接通过配置ThreadPoolExecutor来实现不同特性的线程池.Android中最常见的类具有不同特性的线程池分别为FixThreadPool、CachedhreadPool、SingleThreadPool、ScheduleThreadExecutr.

  1. FixThreadPool:只有核心线程,并且数量固定的,也不会被回收,所有线程都活动时,因为队列没有限制大小,新任务会等待执行。优点:更快的响应外界请求.
  2. SingleThreadPool:只有一个核心线程,确保所有的任务都在同一线程中按序完成.因此不需要处理线程同步的问题.
  3. CachedThreadPool:只有非核心线程,最大线程数非常大,所有线程都活动时会为新任务创建新线程,否则会利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能)处理任务。优点:任何任务都会被立即执行(任务队列SynchronousQuue相当于一个空集合);比较适合执行大量的耗时较少的任务.
  4. ScheduledThreadPool:核心线程数固定,非核心线程(闲着没活干会被立即回收数)没有限制。优点:执行定时任务以及有固定周期的重复任务

29、内存泄露,怎样查找,怎么产生的内存泄露?

  1. 资源对象没关闭造成的内存泄漏
    1. 描述: 资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 Java虚拟机内,还存在于Java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
    2. 程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
  2. 构造Adapter时,没有使用缓存的convertView
    1. 描述: 以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法: public View getView(int position, ViewconvertView, ViewGroup parent) 来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的 view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此可以看出,如果我们不去使用 convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。 ListView回收list item的view对象的过程可以查看: android.widget.AbsListView.java --> voidaddScrapView(View scrap) 方法。 示例代码:
      public View getView(int position, ViewconvertView, ViewGroup parent) {
      View view = new Xxx(...); 
      ... ... 
      return view; 
      }
      
    2. 修正示例代码:
      public View getView(int position, ViewconvertView, ViewGroup parent) {
      View view = null; 
      if (convertView != null) { 
      view = convertView; 
      populate(view, getItem(position)); 
      ... 
      } else { 
      view = new Xxx(...); 
      ... 
      } 
      return view; 
      }
      
  3. Bitmap对象不在使用时调用recycle()释放内存
    1. 描述: 有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的,视情况而定。可以看一下代码中的注释:
      /* •Free up the memory associated with thisbitmap's pixels, and mark the •bitmap as "dead", meaning itwill throw an exception if getPixels() or •setPixels() is called, and will drawnothing. This operation cannot be •reversed, so it should only be called ifyou are sure there are no •further uses for the bitmap. This is anadvanced call, and normally need •not be called, since the normal GCprocess will free up this memory when •there are no more references to thisbitmap. /
      
  4. 试着使用关于application的context来替代和activity相关的context
    1. 这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。更多的请看这篇文章如何避免 Android内存泄漏。
  5. 注册没取消造成的内存泄漏
    1. 一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。 比如:假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个 PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。 但是如果在释放 LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process 进程挂掉。 虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。
  6. 集合中对象没清理造成的内存泄漏
    1. 我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
    2. 查找内存泄漏可以使用Android Studio 自带的AndroidProfiler工具或MAT,也可以使用Square产品的LeakCanary.
1、使用AndroidProfiler的MEMORY工具:

运行程序,对每一个页面进行内存分析检查。首先,反复打开关闭页面5次,然后收到GC(点击Profile MEMORY左上角的垃圾桶图标),如果此时total内存还没有恢复到之前的数值,则可能发生了内存泄露。此时,再点击Profile MEMORY左上角的垃圾桶图标旁的heap dump按钮查看当前的内存堆栈情况,选择按包名查找,找到当前测试的Activity,如果引用了多个实例,则表明发生了内存泄露。

2、使用MAT:
  1. 运行程序,所有功能跑一遍,确保没有改出问题,完全退出程序,手动触发GC,然后使用adb shell dumpsys meminfo packagename -d命令查看退出界面后Objects下的Views和Activities数目是否为0,如果不是则通过Leakcanary检查可能存在内存泄露的地方,最后通过MAT分析,如此反复,改善满意为止。在使用MAT之前,先使用as的Profile中的Memory去获取要分析的堆内存快照文件.hprof,如果要测试某个页面是否产生内存泄漏,可以先dump出没进入该页面的内存快照文件.hprof,然后,通常执行5次进入/退出该页面,然后再dump出此刻的内存快照文件.hprof,最后,将两者比较,如果内存相除明显,则可能发生内存泄露。(注意:MAT需要标准的.hprof文件,因此在as的Profiler中GC后dump出的内存快照文件.hprof必须手动使用Android sdk platform-tools下的hprof-conv程序进行转换才能被MAT打开)
  2. 然后,使用MAT打开前面保存的2份.hprof文件,打开Overview界面,在Overview界面下面有4中action,其中最常用的就是Histogram和Dominator Tree。
    1. Dominator Tree:支配树,按对象大小降序列出对象和其所引用的对象,注重引用关系分析。选择Group by package,找到当前要检测的类(或者使用顶部的Regex直接搜索),查看它的Object数目是否正确,如果多了,则判断发生了内存泄露。然后,右击该类,选择Merge Shortest Paths to GC Root中的exclude all phantom/weak/soft etc.references选项来查看该类的GC强引用链。最后,通过引用链即可看到最终强引用该类的对象。
    2. Histogram:直方图注重量的分析。使用方式与Dominator Tree类似。
  3. 对比hprof文件,检测出复杂情况下的内存泄露:
    1. 通用对比方式:在Navigation History下面选择想要对比的dominator_tree/histogram,右击选择Add to Compare Basket,然后在Compare Basket一栏中点击红色感叹号(Compare the results)生成对比表格(Compared Tables),在顶部Regex输入要检测的类,查看引用关系或对象数量去进行分析即可。
    2. 针对于Historam的快速对比方式:直接选择Histogram上方的Compare to another Heap Dump选择要比较的hprof文件的Historam即可。

30、类的初始化顺序依次是?

(静态变量、静态代码块)>(变量、代码块)>构造方法