2020 年 Android 面试高频知识点小结 V1.0

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2020 年 Android 面试高频知识点小结 V1.0相关的知识,希望对你有一定的参考价值。

文章目录

前言

正直2020金九银十,每一次换工作或是面试候选人,我们都会对面试知识做一次总结梳理,毕竟开发技术无边界,不同人对技术的解读不同。文章总结了最近大半年中的面试考查点V1.0,希望对你有所帮助。

注:整理中有重复的知识点,说明频率较高,同时也是有不同角度的回答,也同时帮你更全面的认识。

面试建议:算法、基础是敲门砖,项目是试金石,良好的面试形象是加分项。

推荐:2020年 中级Android面试总结

Android面试指南 — 算法面试心得

Android性能优化来龙去脉总结

Android面试系列

快手,字节跳动,百度,美团Offer之旅(Android面经分享)| 掘金技术征文

一、JAVA基础

1.synchronized的修饰对象

当synchronized用来修饰静态方法或者类时,将会使得这个类的所有对象都是共享一把类锁,导致线程阻塞,所以这种写法一定要规避

无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

2. trycatchfinally中的执行顺序

任何执行try 或者catch中的return语句之前,都会先执行finally语句,如果finally存在的话。
如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的,
编译器把finally中的return实现为一个warning。

下面是个测试程序
public class FinallyTest  

	public static void main(String[] args) 
		 
		System.out.println(test());;
	

	static int test()
	
		int x = 1;
		try
		
			x++;
			return x;
		
		finally
		
			++x;
		
	

运行结果:2

说明:在try语句中,在执行return语句时,要返回的结果已经准备好了,就在此时,程序转到finally执行了。
在转去之前,try中先把要返回的结果存放到不同于x的局部变量中去,执行完finally之后,在从中取出返回结果,
因此,即使finally中对变量x进行了改变,但是不会影响返回结果。
它应该使用栈保存返回值。

3. JAVA中的死锁

  1. JAVA中的ArrayList是否是线程安全

  2. 为什么ArrayList线程不安全?不安全为什么要使用?如何解决线程不安全?

    首先说一下什么是线程不安全:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。List接口下面有两个实现,一个是ArrayList,另外一个是vector。

    从源码的角度来看,因为Vector的方法前加了,synchronized 关键字,也就是同步的意思,sun公司希望Vector是线程安全的,而希望arraylist是高效的,缺点就是另外的优点。

    。说下原理:一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:

    • 在 Items[Size] 的位置存放此元素;

    • 增大 Size 的值。

      在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
      而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
      那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

    不安全为什么要使用?

    这个ArrayList比线程安全的Vector效率高。

    如何解决线程不安全

    使用synchronized关键字,这个大家应该都很熟悉了,不解释了;

    二:使用Collections.synchronizedList();使用方法如下:

假如你创建的代码如下:

List<Map<String,Object>>data=new ArrayList<Map<String,Object>>();

那么为了解决这个线程安全问题你可以这么使用Collections.synchronizedList(),如:

List<Map<String,Object>> data=Collections.synchronizedList(newArrayList<Map<String,Object>>());

其他的都没变,使用的方法也几乎与ArrayList一样,大家可以参考下api文档;

额外说下 ArrayList与LinkedList;这两个都是接口List下的一个实现,用法都一样,但用的场所的有点不同,ArrayList适合于进行大量的随机访问的情况下使用,LinkedList适合在表中进行插入、删除时使用,二者都是非线程安全,解决方法同上(为了避免线程安全,以上采取的方法,特别是第二种,其实是非常损耗性能的)。
原文链接:https://blog.csdn.net/qq_28081081/article/details/80413669

  1. JAVA和Vector的区别

    首先看这两类都实现List接口,而List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。3个具体实现类的相关区别如下:

    1. ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
    2. Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
    3. LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
    4. vector是线程(Thread)同步(Synchronized)的,所以它也是线程安全的,而Arraylist是线程异步(ASynchronized)的,是不安全的。如果不考虑到线程的安全因素,一般用Arraylist效率比较高。
    5. 如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度
      的50%.如过在集合中使用数据量比较大的数据,用vector有一定的优势。
    6. 如果查找一个指定位置的数据,vector和arraylist使用的时间是相同的,都是0(1),这个时候使用vector和arraylist都可以。而
      如果移动一个指定位置的数据花费的时间为0(n-i)n为总长度,这个时候就应该考虑到使用Linkedlist,因为它移动一个指定位置的数据
      所花费的时间为0(1),而查询一个指定位置的数据时花费的时间为0(i)。
      ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,
      都允许直接序号索引元素,但是插入数据要设计到数组元素移动 等内存操作,所以索引数据快插入数据慢,
      Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差
      ,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快!
    7. 笼统来说:LinkedList:增删改快
      ArrayList:查询快(有索引的存在)

4.synchronized和volatile 关键字的区别

1.volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的, 需要从主存中读取;

​ synchronized 则是锁定当前变量,只有当前线程可以访问该 变量,其他线程被阻塞住。

2.volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级 别的

3.volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以 保证变量的修改可见性和原子性

4.volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

5.volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化

5.Java中的自动装箱和自动拆箱

所以,当 “==”运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。

通过上面的分析我们需要知道两点:
1、什么时候会引发装箱和拆箱
2、装箱操作会创建对象,频繁的装箱操作会消耗许多内存,影响性能,所以可以避免装箱的时候应该尽量避免。

https://zhidao.baidu.com/question/317982664.html

为什么我们在Java中使用自动装箱和拆箱?

6.Java中的乐观锁和悲观锁

  • 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

  • 乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

  • 两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

[Java 多线程之悲观锁与乐观锁]

7.抽象类和接口

如何理解与记忆:

1.抽象类–>像xxx一样
接 口–>能xxx这样

2.接口是设计的结果,抽象类是重构的结果。

抽象类和接口的详解(实例)

8.JAVA内存模型

jmm内存分配的概念:

  • 堆heap: 优点:运行时数据区,动态分配内存大小,有gc;,缺点:因为要在运行时动态分配,所以存取速度慢,对象存储在堆上,静态类型的变量跟着类的定义一起存储在堆上。
    栈stack:存取速度快,仅次于寄存器,缺点:数据大小与生存期必须是确定的,缺乏灵活性,栈中主要存放基本类型变量(比如,int,shot,byte,char,double,foalt,boolean和对象句柄),jmm要求,调用栈和本地变量存放在线程栈上

当一个线程可以访问一个对象时,也可以访问对象的成员变量,如果有两个线程访问对象的成员变量,则每个线程都有对象的成员变量的私有拷贝。

  • 处理器(cpu): 寄存器:每个cpu都包含一系列寄存器,他们是cpu的基础,寄存器执行的速度,远大于在主存上执行的速度

  • cpu高速缓存:由于处理器与内存访问速度差距非常大,所以添加了读写速度尽可能接近处理器的高速缓存,来作为内存与处理器之间的缓冲,将数据读到缓存中,让运算快速进行,当运算结束,再从缓存同步到主存中,就无须等待缓慢的内存读写了。处理器访问缓存的速度快与访问主存的速度,但比访问内部寄存器的速度还是要慢点,每个cpu有一个cpu的缓存层,一个cpu含有多层缓存,,某一时刻,一个或者多个缓存行可能同时被读取到缓存取,也可能同时被刷新到主存中,同一时刻,可能存在多个操作,

  • 内存:一个计算机包含一个主存,所有cpu都可以访问主存,主存通常远大于cpu中的缓存,
    运作原理: 通常,当一个cpu需要读取主存时,他会将主存的内容读取到缓存中,将缓存中的内容读取到内部寄存器中,在寄存器中执行操作,当cpu需要将结果回写到主存中时,他会将内部寄存器的值刷新到缓存中,然后会在某个时间点将值刷新回主存。

  • ……

原文链接:https://blog.csdn.net/wangnanwlw/article/details/86466782

9.GC Roots如何确定?哪些对象可以作为GC Roots?

原文链接:https://blog.csdn.net/weixin_41910694/article/details/90706652

  • 判断对象是否可以被回收之引用计数法:

Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。
因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。

任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。

那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。

  • 判断对象是否可以被回收之枚举根节点可达性分析

    为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
    所谓"GC roots,或者说tracing GC的“根集合”就是一组必须活跃的引用。
    基本思路就是通过一系列名为”GCRoots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GCRoots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的就自然被判定为死亡。

  • Java中可以作为GC Roots的对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的native方法)中引用的对象

9.抽象类和接口的区别

10. 四种引用的区别

二、android方面

1.热修复的原理

我们知道Java虚拟机 —— JVM 是加载类的class文件的,而Android虚拟机——Dalvik/ART VM 是加载类的dex文件,

而他们加载类的时候都需要ClassLoader,ClassLoader有一个子类BaseDexClassLoader,

而BaseDexClassLoader下有一个数组——DexPathList,是用来存放dex文件,当BaseDexClassLoader通过调用findClass方法时,实际上就是遍历数组,找到相应的dex文件,找到则直接将它return。

而热修复的解决方法就是将新的dex添加到该集合中,并且是在旧的dex的前面,

所以就会优先被取出来并且return返回。

2.Android中跨进程通讯的几种方式

Android 跨进程通信,像intent,contentProvider,广播,service都可以跨进程通信。

intent:这种跨进程方式并不是访问内存的形式,它需要传递一个uri,比如说打电话。

contentProvider:这种形式,是使用数据共享的形式进行数据共享。

service:远程服务,aidl

3.AIDL理解

此处延伸:简述Binder

AIDL: 每一个进程都有自己的Dalvik VM实例,都有自己的一块独立的内存,都在自己的内存上存储自己的数据,执行着自己的操作,都在自己的那片狭小的空间里过完自己的一生。而aidl就类似与两个进程之间的桥梁,使得两个进程之间可以进行数据的传输,跨进程通信有多种选择,比如 BroadcastReceiver , Messenger 等,但是 BroadcastReceiver 占用的系统资源比较多,如果是频繁的跨进程通信的话显然是不可取的;Messenger 进行跨进程通信时请求队列是同步进行的,无法并发执行。

Binde机制简单理解:

在Android系统的Binder机制中,是有Client,Service,ServiceManager,Binder驱动程序组成的,其中Client,service,Service Manager运行在用户空间,Binder驱动程序是运行在内核空间的。而Binder就是把这4种组件粘合在一块的粘合剂,其中核心的组件就是Binder驱动程序,Service Manager提供辅助管理的功能,而Client和Service正是在Binder驱动程序和Service Manager提供的基础设施上实现C/S 之间的通信。其中Binder驱动程序提供设备文件/dev/binder与用户控件进行交互,

Client、Service,Service Manager通过open和ioctl文件操作相应的方法与Binder驱动程序进行通信。而Client和Service之间的进程间通信是通过Binder驱动程序间接实现的。而Binder Manager是一个守护进程,用来管理Service,并向Client提供查询Service接口的能力。

4.Android内存泄露及管理 (深度延伸下去)

(1)内存溢出(OOM)和内存泄露(对象无法被回收)的区别。

(2)引起内存泄露的原因

(3) 内存泄露检测工具 ------>LeakCanary

内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。内存溢出通俗的讲就是内存不够用。

内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光

内存泄露原因:

一、Handler 引起的内存泄漏。

解决:将Handler声明为静态内部类,就不会持有外部类SecondActivity的引用,其生命周期就和外部类无关,

如果Handler里面需要context的话,可以通过弱引用方式引用外部类

二、单例模式引起的内存泄漏。

解决:Context是ApplicationContext,由于ApplicationContext的生命周期是和app一致的,不会导致内存泄漏

三、非静态内部类创建静态实例引起的内存泄漏。

解决:把内部类修改为静态的就可以避免内存泄漏了

四、非静态匿名内部类引起的内存泄漏。

解决:将匿名内部类设置为静态的。

五、注册/反注册未成对使用引起的内存泄漏。

注册广播接受器、EventBus等,记得解绑。

六、资源对象没有关闭引起的内存泄漏。

在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等方法释放。

5.Java虚拟机和Dalvik虚拟机的区别

Java虚拟机:

1、java虚拟机基于栈。 基于栈的机器必须使用指令来载入和操作栈上数据,所需指令更多更多。

2、java虚拟机运行的是java字节码。(java类会被编译成一个或多个字节码.class文件)

Dalvik虚拟机:

1、dalvik虚拟机是基于寄存器的

2、Dalvik运行的是自定义的.dex字节码格式。(java类被编译成.class文件后,会通过一个dx工具将所有的.class文件转换成一个.dex文件,然后dalvik虚拟机会从其中读取指令和数据

3、常量池已被修改为只使用32位的索引,以 简化解释器。

4、一个应用,一个虚拟机实例,一个进程(所有android应用的线程都是对应一个linux线程,都运行在自己的沙盒中,不同的应用在不同的进程中运行。每个android dalvik应用程序都被赋予了一个独立的linux PID(app_*))

6.四种LaunchMode及其使用场景(结合书中上场景再总结)

standard 模式

这是默认模式,每次激活Activity时都会创建Activity实例,并放入任务栈中。使用场景:大多数Activity。

singleTop 模式

如果在任务的栈顶正好存在该Activity的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该Activity的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类App的内容页面。

singleTask 模式

如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。

singleInstance 模式

在一个新栈中创建该Activity的实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity实例已经存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例( 会调用实例的 onNewIntent() )。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

7.启动模式 (其他应用场景)

  1. standard 标准模式
  2. singleTop 栈顶复用模式 (例如:推送点击消息界面)
  3. singleTask 栈内复用模式 (例如:首页)
  4. singleInstance 单例模式 (单独位于一个任务栈中,例如:拨打电话界面)

8. 进程 IPC 进程通讯方式

  • Intent 、Bundle : 要求传递数据能被序列化,实现 Parcelable、Serializable ,适用于四大组件通信。

  • 文件共享 :适用于交换简单的数据实时性不高的场景。

  • AIDL:AIDL 接口实质上是系统提供给我们可以方便实现 BInder 的工具

  • Messenger:基于 AIDL 实现,服务端串行处理,主要用于传递消息,适用于低并发一对多通信

  • ContentProvider:基于 Binder 实现,适用于一对多进程间数据共享(通讯录 短信 等)

  • Socket:TCP、UDP,适用于网络数据交换

9.为什么要用Binder?(有什么优势?)

  • Android使用的Linux内核 拥有有着非常多的跨进程通信机制
  • 性能
  • 安全

10.View 工作流程

通过 SetContentView(),调用 到PhoneWindow ,后实例DecorView ,通过 LoadXmlResourceParser() 进行IO操作 解析xml文件 通过反射 创建出View,并将View绘制在 DecorView上,这里的绘制则交给了ViewRootImpl 来完成,通过performTraversals() 触发绘制流程,performMeasure 方法获取View的尺寸,performLayout 方法获取View的位置 ,然后通过 performDraw 方法遍历View 进行绘制。

11.事件分发

一个 MotionEvent 产生后,按 Activity -> Window -> DecorView(ViewGroup) -> View 顺序传递,View 传递过程就是事件分发,因为开发过程中存在事件冲突,所以需要熟悉流程:

  • dispatchTouchEvent:用于分发事件,只要接受到点击事件就会被调用,返回结果表示是否消耗了当前事件
  • onInterceptTouchEvent:用于判断是否拦截事件(只有ViewGroup中存在),当 ViewGroup 确定要拦截事件后,该事件序列都不会再触发调用此 ViewGroup 的 onIntercept
  • onTouchEvent:用于处理事件,返回结果表示是否处理了当前事件,未处理则传递给父容器处理。(事件顺序是:OnTouchListener -> OnTouchEvent -> OnClick)

12. Handler机制整体流程;

IdHandler(闲时机制);

postDelay()的具体实现;

post()与sendMessage()区别;

使用Handler需要注意什么问题,怎么解决的?

问题很细,能准备多详细就准备多详细。人家自己封装了一套 Handler 来避免内存泄漏问题

13.Looper.loop()为什么不会阻塞主线程;

https://www.jianshu.com/p/e149a2e89290

主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗

主线程中如果没有looper进行循环,那么主线程一运行完毕就会退出。那么我们还能运行APP吗,显然,这是不可能的,Looper主要就是做消息循环,然后由Handler进行消息分发处理,一旦退出消息循环,那么你的应用也就退出了。

总结:Looper的无限循环必不可少。

补充说明:

我看有一部分人理解”Looper.loop()的阻塞“和”UI线程上执行耗时操作卡死“的区别时还一脸懵逼的状况,简单回答一波:

  1. 首先这两之间一点联系都没有,完全两码事。

  2. Looper上的阻塞,前提是没有输入事件,MsgQ为空,Looper空闲状态,线程进入阻塞,释放CPU执行权,等待唤醒。

  3. UI耗时导致卡死,**前提是要有输入事件,**MsgQ不为空,Looper正常轮询,线程并没有阻塞,但是该事件执行时间过长(5秒?),而且与此期间其他的事件(按键按下,屏幕点击…)都没办法处理(卡死),然后就ANR异常了。

    链接:https://www.zhihu.com/question/34652589/answer/278867983
    来源:知乎

14 . Android – Looper.prepare()和Looper.loop() —深入版

Android中的Looper类,是用来封装消息循环和消息队列的一个类,用于在android线程中进行消息处理。handler其实可以看做是一个工具类,用来向消息队列中插入消息的。

​ (1) Looper类用来为一个线程开启一个消息循环。 默认情况下android中新诞生的线程是没有开启消息循环的。(主线程除外,主线程系统会自动为其创建Looper对象,开启消息循环。) Looper对象通过MessageQueue来存放消息和事件。一个线程只能有一个Looper,对应一个MessageQueue。

(2) 通常是通过Handler对象来与Looper进行交互的。Handler可看做是Looper的一个接口,用来向指定的Looper发送消息及定义处理方法。 默认情况下Handler会与其被定义时所在线程的Looper绑定,比如,Handler在主线程中定义,那么它是与主线程的Looper绑定。 mainHandler = new Handler() 等价于new Handler(Looper.myLooper()). Looper.myLooper():获取当前进程的looper对象,类似的 Looper.getMainLooper() 用于获取主线程的Looper对象。

(3) 在非主线程中直接new Handler() 会报如下的错误:

E/AndroidRuntime( 6173): Uncaught handler: thread Thread-8 exiting due to uncaught exception E/AndroidRuntime( 6173): java.lang.RuntimeException: Can’t create handler inside thread that has not called Looper.prepare()

原因是非主线程中默认没有创建Looper对象,需要先调用Looper.prepare()启用Looper。

(4) Looper.loop();

让Looper开始工作,从消息队列里取消息,处理消息。

注意:写在Looper.loop()之后的代码不会被执行,这个函数内部应该是一个循环,当调用mHandler.getLooper().quit()后,loop才会中止,其后的代码才能得以运行。
(5) 基于以上知识,可实现主线程给子线程(非主线程)发送消息。

15.线程的切换又是怎么回事?

那么线程的切换又是怎么回事呢?
很多人搞不懂这个原理,但是其实非常简单,我们将所涉及的方法调用栈画出来,如下:

Thread.foo()
    Looper.loop()
     -> MessageQueue.next()
      -> Message.target.dispatchMessage()
       -> Handler.handleMessage()

显而易见,Handler.handleMessage() 所在的线程最终由调用 Looper.loop() 的线程所决定。

平时我们用的时候从异步线程发送消息到 Handler,这个 Handler 的 handleMessage() 方法是在主线程调用的,所以消息就从异步线程切换到了主线程。

作者:Android架构小麦
链接:https://juejin.im/post/5eca83a06fb9a047f93726c9

16. Handler是如何实现线程之间的切换的

Handler是如何实现线程之间的切换的呢?例如现在有A、B两个线程,在A线程中有创建了handler,然后在B线程中调用handler发送一个message。

通过上面的分析我们可以知道,当在A线程中创建handler的时候,同时创建了MessageQueue与Looper,Looper在A线程中调用loop进入一个无限的for循环从MessageQueue中取消息,当B线程调用handler发送一个message的时候,会通过msg.target.dispatchMessage(msg);将message插入到handler对应的MessageQueue中,Looper发现有message插入到MessageQueue中,便取出message执行相应的逻辑,因为Looper.loop()是在A线程中启动的,所以则回到了A线程,达到了从B线程切换到A线程的目的。

小结:

1.Handler初始化之前,Looper必须初始化完成。UI线程之所以不用初始化,因为在ActivityThread已经初始化,其他子线程初始化Handler时,必须先调用Looper.prepare()。

2.通过Handler发送消息时,消息会回到Handler初始化的线程,而不一定是主线程。

3.使用ThreadLocal时,需要注意内存泄漏的问题。

通俗点的说法Handler机制其实就是借助共享变量来进行线程切换的.

Handler是如何实现线程之间的切换的

妙用 Looper 机制

我们可以利用 Looper 的机制来帮助我们做一些事情:

  1. 将 Runnable post 到主线程执行;
  2. 利用 Looper 判断当前线程是否是主线程。

完整示例代码如下:

public final class MainThread 

    private MainThread() 
    

    private static final Handler HANDLER = new Handler(Looper.getMainLooper());

    public static void run(@NonNull Runnable runnable) 
        if (isMainThread()) 
            runnable.run();
        else
            HANDLER.post(runnable);
        
    

    public static boolean isMainThread() 
        return Looper.myLooper() == Looper.getMainLooper();
    


能够省去不少样板代码。

作者:Android架构小麦
链接:https://juejin.im/post/5eca83a06fb9a047f93726c9

明确我们的问题

  1. Handler 是如何与线程关联的?
  2. Handler 发出去的消息是谁管理的?
  3. 消息又是怎么回到 handleMessage() 方法的?
  4. 线程的切换是怎么回事?

回答:Handler 发送的消息由 MessageQueue 存储管理,并由 Loopler 负责回调消息到 handleMessage()。

线程的转换由 Looper 完成,handleMessage() 所在线程由 Looper.loop() 调用者所在线程决定。

10.Android为什么推荐使用SparseArray来替代HashMap?

SparseArray有两个优点:

1.避免了自动装箱(auto-boxing)

2.数据结构不会依赖于外部对象映射。我们知道HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置,存放的都是数组元素的引用,通过每个对象的hash值来映射对象。而SparseArray则是用数组数据结构来保存映射,然后通过折半查找来找到对象。但其实一般来说,SparseArray执行效率比HashMap要慢一点,因为查找需要折半查找,而添加删除则需要在数组中执行,而HashMap都是通过外部映射。但相对来说影响不大,最主要是SparseArray不需要开辟内存空间来额外存储外部映射,从而节省内存。
https://blog.csdn.net/woshizisezise/article/details/79361458

11.Glide缓存机制

12.Binder机制

13.内存泄漏

查找内存泄漏可以使用Android Studio 自带的AndroidProfiler工具或MAT,也可以使用Square产品的 LeakCanary.

14.类的初始化顺序依次是?

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

15.Retrofit库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?

Retrofit主要是在create方法中采用动态代理模式(通过访问代理对象的方式来间接访问目标对象)实 现接口方法,这个过程构建了一个ServiceMethod对象,根据方法注解获取请求方式,参数类型和参数 注解拼接请求的链接,当一切都准备好之后会把数据添加到Retrofit的RequestBuilder中。然后当我们 主动发起网络请求的时候会调用okhttp发起网络请求,okhttp的配置包括请求方式,URL等在Retrofit 的RequestBuilder的build()方法中实现,并发起真正的网络请求。

你从这个库中学到什么有价值的或者说可借鉴的设计思想?

内部使用了优秀的架构设计和大量的设计模式,在我分析过Retrofit最新版的源码和大量优秀的Retrofit 源码分析文章后,我发现,要想真正理解Retrofit内部的核心源码流程和设计思想,首先,需要对它使 用到的九大设计模式有一定的了解,下面我简单说一说:

1、创建Retrofit实例: 使用建造者模式通过内部Builder类建立了一个Retroift实例。 网络请求工厂使用了工厂方法模式。

2、创建网络请求接口的实例:

首先,使用外观模式统一调用创建网络请求接口实例和网络请求参数配置的方法。 然后,使用动态代理动态地去创建网络请求接口实例。
接着,使用了建造者模式 & 单例模式创建了serviceMethod对象。

再者,使用了策略模式对serviceMethod对象进行网络请求参数配置,即通过解析网络请求接口方 法的参数、返回值和注解类型,从Retrofit对象中获取对应的网络的url地址、网络请求执行器、网 络请求适配器和数据转换器。

最后,使用了装饰者模式ExecuteCallBack为serviceMethod对象加入线程切换的操作,便于接受 数据后通过Handler从子线程切换到主线程从而对返回数据结果进行处理。

3、发送网络请求: 在异步请求时,通过静态delegate代理对网络请求接口的方法中的每个参数使用对应的

ParameterHanlder进行解析。

4、解析数据

5、切换线程: 使用了适配器模式通过检测不同的Platform使用不同的回调执行器,然后使用回调执行器切换线

程,这里同样是使用了装饰模式。

6、处理结果

16.ARouter路由原理:

ARouter维护了一个路由表Warehouse,其中保存着全部的模块跳转关系,ARouter路由跳转实际上还 是调用了startActivity的跳转,使用了原生的Framework机制,只是通过apt注解的形式制造出跳转规 则,并人为地拦截跳转和设置跳转条件。

17.Glide中的动态代理

代理模式的介绍

18.服务的两种启动方式和使用场景(两次都问到)

19. 内存抖动

Gc 引起卡顿+OOM,怎么优化

Gson反序列化导致产生大量对象

解决思考:对象池

20.LeakCanary原理

它的基本工作原理如下:

RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

总的来说,LeakCanary有如下几个明显优点:

针对Android Activity组件完全自动化的内存泄漏检查。
可定制一些行为(dump文件和leaktrace对象的数量、自定义例外、分析结果的自定义处理等)。
集成到自己工程并使用的成本很低。
友好的界面展示和通知。
原文链接:https://blog.csdn.net/import_sadaharu/article/details/81407728

21.如何做内存优化

22.ArrayMap和HashMap的区别

HashMap和ArrayMap各自的优势
1.查找效率:
HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。
ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降。
所以对于Map数量比较大的情况下,推荐使用
2.扩容数量:
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申请4个。这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。
因此,如果当数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。
3.扩容效率:
HashMap每次扩容的时候时重新计算每个数组成员的位置,然后放到新的位置。
ArrayMap则是直接使用System.arraycopy。
所以效率上肯定是ArrayMap更占优势。这里需要说明一下,网上有一种传闻说因为ArrayMap使用System.arraycopy更省内存空间,这一点我真的没有看出来。arraycopy也是把老的数组的对象一个一个的赋给新的数组。当然效率上肯定arraycopy更高,因为是直接调用的c层的代码。
4.内存耗费:
以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。由于ArrayMap只缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的节省内存的。
5.总结:
综上所述,数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。
而数据量比较大的时候,则推荐使用HashMap。

原文链接:https://blog.csdn.net/zuo_er_lyf/article/details/90598937

23.HashMap原理

数据结构和算法思考

1.为什么选择数组和链表结构?

①数组内存连续块分配,效率体现查询更快。HashMap中用作查找数组桶的位置,利用元素的key的hash值对数组长度取模得到。

②链表效率体现增加和删除。HashMap中链表是用来解决hash冲突,增删空间消耗平衡。

**扩展:**为什么不是ArrayList而是使用Node<K,V>[] tab?因为ArrayList的扩容机制是1.5倍扩容,而HashMap扩容是2的次幂。

2.HashMap出现线程问题

①多线程扩容,引起的死循环问题(jdk1.8中,死循环问题已经解决)。
②多线程put的时候可能导致元素丢失
③put非null元素后get出来的却是null

3.使用线程安全Map

①HashMap并不是线程安全,要实现线程安全可以用Collections.synchronizedMap(m)获取一个线程安全的HashMap。
②CurrentHashMap和HashTable是线程安全的。CurrentHashMap使用分段锁技术,要操作节点先获取段锁,在修改节点。

4.Android提倡使用ArrayMap

①ArrayMap数据结构是两个数组,一个存放hash值,另一个存放key和value。
②根据key的hash值利用二分查找在hash数组中找出index。
③根据index在key-value数组中对应位置查找,如果不相等认为冲突了,会以key为中心,分别上下展开,逐一查找。
优势,数据量少时(少于1000)相比HashMap更节省内存。劣势,删除和插入时效率要比HashMap要低。

kotlin协程的使用与原理

24.多线程间通信和多进程之间通信有什么不同,分别怎么实现?

1、进程间的通信方式