一些面试容易问的知识点—Android+Java

如题,这里主要列举的是Android开发相关的,但感觉安全也会需要用到。包括Android消息机制Handler、Android事件分发、Android内存泄漏。

1.Android消息机制Handler

Android的消息机制主要是Handler的运行机制,Handler的运行需要底层MessageQueue和looper的支撑。Handler机制被引入的目的就是为了实现线程间通信。Handler一共干了两件事——

  • 在子线程中发出massage;
  • 在主线程获取,处理message。

A.概述

  • 消息机制中主要用于多线程的通讯。
  • 在 Android 开发中最常见的使用场景是:在子线程做耗时操作,操作完成后需要在主线程更新 UI(子线程不能直接修改 UI)。
  • 这时就需要用到消息机制来完成子线程和主线程的通讯。
  • Message 是消息机制中消息的载体。

B.实际应用

  • 在项目中用过Handler.postDelayed()方法。
  • 主要是新建一个线程Runnable,在线程中每隔一段时间进行某些判断和操作。
  • handler在这里主要承担的是子线程中延时的作用。
  • 也就是将指定Runnable(包装成PostMessage)加入到MessageQueue中,然后Looper不断从MessageQueue(链表)中读取Message进行处理。
  • 在项目中是每隔30秒进行Runnable中的处理和操作。

C.关于Handler.postDelayed()

如何保证Handler.postDelayed()发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列?

举个例子说明——

  1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
  2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
  3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
  4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;
  5. 直到阻塞时间到或者下一次有Message进队;

总结一下,就是MessageQueue会根据post delay的时间排序放入到链表中,链表头的时间小,尾部时间最大。因此能保证时间Delay最长的不会block住时间短的。当每次post message的时候会进入到MessageQueue的next()方法,会根据其delay时间和链表头的比较,如果更短则,放入链表头,并且看时间是否有delay,如果有,则block,等待时间到来唤醒执行,否则将唤醒立即执行。

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

使用后者的方式,集中式的统一管理了所有message,而如果像前者的话,有多少个delay message,则需要起多少个定时器。前者由于有了排序,而且保存的每个message的执行时间,因此只需一个定时器按顺序next即可。

D.消息机制的具体流程

(1)准备阶段

  • 在子线程调用 Looper.prepare() 方法或 在主线程调用 。Lopper.prepareMainLooper() 方法创建当前线程的 Looper 对象(主线程中这一步由 Android 系统在应用启动时完成)。
  • 在创建 Looper 对象时会创建一个消息队列 MessageQueue。
  • Looper 通过 loop() 方法获取到当前线程的 Looper 并启动循环,从 MessageQueue 不断提取 Message,若 MessageQueue 没有消息,处于阻塞状态。

(2)发送消息

  • 使用当前线程创建的 Handler 在其它线程通过 sendMessage() 发送 Message 到 MessageQueue。
  • MessageQueue 插入新 Message 并唤醒阻塞。

(3)获取消息

  • 重新检查 MessageQueue 获取新插入的 Message。
  • Looper 获取到 Message 后,通过 Message 的 target 即 Handler 调用 dispatchMessage(Message msg) 方法分发提取到的 Message,然后回收 Message 并继续循环获取下一个 Message。
  • Handler 使用 handlerMessage(Message msg) 方法处理 Message。

(4)阻塞等待

  • MessageQueue 没有 Message 时,重新进入阻塞状态。

E.扩展——Thread、Looper和Handler

在Android中——

  • 接收消息的“消息队列” ——【MessageQueue】
  • 阻塞式地从消息队列中接收消息并进行处理的“线程” ——【Thread+Looper】
  • 可发送的“消息的格式” ——【Message】
  • “消息发送函数”——【Handler的post和sendMessage】

一个Looper类似一个消息泵。它本身是一个死循环,不断地从MessageQueue中提取Message或者Runnable。而Handler可以看做是一个Looper的暴露接口,向外部暴露一些事件,并暴露sendMessage()和post()函数。

由于每一个线程内最多只可以有一个Looper,所以一定要在Looper.prepare()之前做好判定,否则会抛出java.lang.RuntimeException: Only one Looper may be created per thread。为了获取Looper的信息可以使用两个方法:

  • Looper.myLooper()
  • Looper.getMainLooper()

Looper.myLooper()获取当前线程绑定的Looper,如果没有返回null。Looper.getMainLooper()返回主线程的Looper,这样就可以方便的与主线程通信。注意:在Thread的构造函数中调用Looper.myLooper只会得到主线程的Looper,因为此时新线程还未构造好。

F.主要参考链接

Android 消息机制详解
你真的懂Handler.postDelayed()的原理吗?
Android中的Thread, Looper和Handler机制(附带HandlerThread与AsyncTask)
Android-Handler消息处理机制

2.Android事件分发

A.概述

  • 将点击事件(MotionEvent)传递到某个具体的View & 处理的整个过程;
  • 事件传递的顺序:Activity -> ViewGroup -> View;
  • 要想充分理解Android分发机制,本质上是要理解:Activity对点击事件的分发机制、ViewGroup对点击事件的分发机制、View对点击事件的分发机制。

B.主要参考链接

Android事件分发机制 详解攻略,您值得拥有

3.Android内存泄漏(重点)

A.概述

产生原因:在 Android 中内存泄漏的原因其实和在 Java 中是一样的,即某个对象已经不需要再用了,但是它却没有被系统所回收,一直在内存中占用着空间,而导致它无法被回收的原因大多是由于它被一个生命周期更长的对象所引用。即生命周期较长的对象持有生命周期较短的对象的引用,导致生命周期较短的对象在使用完成后也无法被内存回收机制回收。

在了解Android内存泄漏之前,需要先理解Java的内存分配策略以及垃圾回收机制(也就是我们所说的GC)。

B.Java内存管理相关

(1)Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)栈区堆区

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在

  • 栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例这部分内存在不使用时将会由 Java 垃圾回收器来负责回收

(2)Java中堆与栈的区别

  • 在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。
  • 当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

  • 堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。

  • 在堆中分配的内存,将由 Java 垃圾回收器来自动管理。
  • 在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。
  • 我们可以通过这个引用变量来访问堆中的对象或者数组。

(3)Java垃圾回收(GC)

A.概述
  • Java的内存管理就是对象的分配和释放问题。
  • 在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。
  • 另外,对象的释放是由 GC 决定和执行的。
  • 在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的。
  • GC 为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。
  • GC在简化程序员的工作的同时,加重了JVM的工作,这也是Java程序运行速度较慢的原因之一。
  • Java使用有向图的方式进行内存管理,具体的描述见下图。

B.java内存泄漏

在Java中,内存泄漏就是指存在一些满足如下条件的被分配的对象,这些对象不会被GC所回收,然而它却占用内存。

  • 这些对象是可达的,即在有向图中,存在通路可以与其相连;
  • 这些对象是无用的,即程序以后不会再使用这些对象;

一、java内存泄漏的原因:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。主要有以下类:

  • 静态集合类引起内存泄漏
  • 当集合里面的对象属性被修改后,再调用remove()方法时不起作用(?)
  • 监听器
  • 各种连接
  • 内部类和外部模块的引用
  • 单例模式

二、java的内存泄漏与c++内存泄漏的比较


C.JVM调用GC的时间和策略

(4)Java 对象的几种引用类型

Java对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种。

C.Android内存泄漏

(1)常见内存泄漏

  • 集合类泄漏(集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。)
  • 单例造成的内存泄漏(单例的静态特性使得其生命周期跟应用的生命周期一样长)
  • 匿名内部类/非静态内部类和异步线程

android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露。

  • Handler 造成的内存泄漏

Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

  • 尽量避免使用 static 成员变量(如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。)

  • 避免 override finalize()

1、finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是: 虚拟机调用GC的时间不确定 Finalize daemon线程被调度到的时间不确定

2、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:

含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。

3、含有Finalize方法的object需要至少经过两轮GC才有可能被释放。

  • 资源未关闭造成的内存泄漏(对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。)

  • 一些不良代码造成的内存压力

(2)如何避免内存泄漏

  1. 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。

  2. 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

  3. 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

  • 将内部类改为静态内部类
  • 静态内部类中使用弱引用来引用外部类的成员变量
  1. Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.

  2. 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。

  3. 正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。

  4. 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

D.主要参考链接

Android 中内存泄漏的原因和解决方案
Android内存泄漏总结

4.java相关

A.java的接口和抽象类的区别

  • 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

B.java线程池

由浅入深理解Java线程池及线程池的如何使用

为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。

线程池的任务处理策略:

  1. 如果当前线程池中的线程数目小于corePoolSize(表示允许线程池中允许同时运行的最大线程数),则每来一个任务,就会创建一个线程去执行这个任务;

  2. 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize(线程池允许的最大线程数,表示最大能创建多少个线程),则会采取任务拒绝策略进行处理;

  3. 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime(表示线程没有任务时最多保持多久然后停止),线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

拒绝策略:

AbortPolicy:丢弃任务并抛出RejectedExecutionException

CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

DiscardPolicy:丢弃任务,不做任何处理。

线程池的正常使用:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:

  • newFixedThreadPoolnewSingleThreadExecutor:
      主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPoolnewScheduledThreadPool:
      主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM

C.HashMap和hashTable的主要区别

HashTable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行。简单来说就是,如果你不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。HashTable已经被淘汰了,不要在新的代码中再使用它。

HashMap和HashTable到底哪不同?

D.类加载和双亲委派机制

(1)类加载

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  1. 启动类加载器:Bootstrap ClassLoader。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

  2. 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

(2)双亲委派机制

概述

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

意义—— 防止内存中出现多份同样的字节码

比如两个类A和类B都要加载System类:

  • 如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
  • 如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。

5.Android相关

A.组件的生命周期

Android四大组件生命周期
Android四大组件

(1)Activity的生命周期

七大生命周期 :onCreate 不可见不可交互、 onStart 可见不可交互 、 onResume 可见可交互 、 onPause 可见不可交互 、onStop 不可见不可交互 、 onDestroy 销毁了 、 onRestart 从不可见到可见 ;

(2)Fragment生命周期

Fragment和Activity的区别

  • Fragment是到Android3.0+ 以后,Android新增了Fragments,在没有 Fragment 之前,一个屏幕只能放一个 Activity。
  • Activity 代表了一个屏幕的主体,而Fragment可以作为Activity的一个组成元素。一个Activity可以有若干个(0或n)Fragment构成。
  • Fragment 从功能上讲相当于一个子活动(Activity),它可以让多个活动放到同一个屏幕上,也就是对用户界面和功能的重用,因为对于大屏设备来说,纯粹的 Activity 有些力不从心。
  • Fragment 像是一个子活动,但是 Fragment 不是 Activity 的扩展,因为 Fragment 扩展自 android.app 中的 Object,而 Activity 是 Context 的子类。
  • Fragment不能脱离Activity而存在,只有Activity才能作为接收intent的载体。其实两者基本上是载体和组成元素的关系。

(3)Service生命周期

B.Android常用的6种布局

(1)LinearLayout(线性布局)

显示特点:所有子控件按照横向或者竖向依次排列,android:orientation=”vertical”(竖向),android:orientation=”horizontal”(横向)。

children排列成一行多列或者一列多行的形式,应该是应用程序中最常用的布局方式,它提供了控件水平或者垂直排列的模型,同时我们可通过设置子控件的weight布局参数控制各个控件在布局中的相对大小。

(2)FrameLayout(帧布局)

所有的子控件默认显示在FrameLayout的左上角,会重叠在一起显示。最简单的布局模型,在这种布局下每个添加的子控件都被放在布局的左上角,并覆盖在前一子控件的上层。

(3)RelativeLayout(相对布局)

children是相互之间相关位置或者和他们的parent位置相关,参照控件可以是父控件,也可以是其他子控件,但被参照的控件必须要在参照它的控件之前定义。

(4)AbsoluteLayout(绝对布局)

子控件需要指定相对于此坐标布局的横、纵坐标值,否则将会像FrameLayout那样被排在左上角。手机应用需要适用于不同的屏幕大小,而这种布局模型不能自适应屏幕尺寸大小,所以应用得相当少。

(5)TableLayout(表格布局)

适用于多行多列的布局格式,每个TableLayout是由多个TableRow组成,一个TableRow就表示TableLayout中的每一行,这一行可以由多个子元素组成。实际上TableLayout和TableRow都是LineLayout线性布局的子类。但是TableRow的参数android:orientation属性值固定为horizontal,且android:layout_width=MATCH_PARENT,android:layout_height=WRAP_CONTENT。所以TableRow实际是一个横向的线性布局,且所以子元素宽度和高度一致。

(6)GridLayout(网格布局)

Android 六大布局之 GridLayout(网格布局)

GridLayout 布局是 Android 4.0 以后引入的新布局,和 TableLayout(表格布局) 有点类似,不过它功能更多,也更加好用。