offce 002

垃圾回收算法

标记算法:

1.引用计数法
2.可达性分析法(注意GC root的类型,虚拟机栈和本地方法栈引用的对象、静态对象、字节码对象)

回收算法(复制算法、标记清除、标记整理)

新生代:对象存活率低,采用复制算法,堆中分为3个区域,Eden、from、to,每次分配对象都在Eden,第一次gc时,把存活对象复制到from,第二次gc把Eden和from的对象复制到to,第三次又把Eden和to的对象复制到from,依次往复。达到一定阈值时,把对象移入老年代。

老年代:对象存活率高,标记整理法

java类加载机制

  • 双亲委托机制:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完

  • 成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载

  • Java的类加载器:根加载器(加载java核心类)、扩展类加载器(加载jre扩展目录)、应用类加载器(加载classPath指定目录的类,自定义类加载器一般继承此加载器)

  • Android的类加载器:根加载器、BaseDexClassLoader、PathClassLoader(加载安装到系统的APK)、DexClassLoader(加载指定目录的dex和apk)

java匿名内部类

匿名内部类就是没有名字的内部类,(其实是有名字的,虚拟机定位这个类,编译之后会使用 外部类名$1这样的名字,数字按顺序生成)。
匿名内部类的构造方法由编译器生成,参数列表包括:

  • 外部类的引用(定义在非静态域)

  • 捕获的外部变量(方法体中使用的外部final对象)

  • 父类的构造参数

  • 如果父类也是一个非静态内部类则还有父类的外部类引用。

注意:

  • 不能继承父类或者实现接口(kotlin中是可以的)

  • 不能定义静态变量和方法

  • 会持有外部类的引用,可能会造成内存泄露。

拓展:lambda表达式可以替代部分匿名内部类,父类必须是接口,且只有一个方法。

java泛型擦除

使用泛型可以声明集合存储的元素类型,取出元素时避免强转的操作。在java中,编译完成后泛型参数会被擦除,例如List和List编译完成后都是List类型。

java泛型为什么会被擦除:

  • 运行时内存压力小,不同泛型的List都编译成同一个类型。泛型不擦除的语言如c#,在方法区就会真实存在各种不同的List类型,压力就会相对较大。

  • 兼容性的问题,1.5之前是没有泛型的,java当时的用户量很大,为了向下兼容。

存在的问题:

1.基本类型无法用于泛型,只能用装箱类型,例如List,装箱操作有额外的开销。
2.泛型参数不能用于方法重载,因为编译完成后泛型被擦除,参数都是一样的。
3.泛型类型不能当做真实的类型来使用,例如方法参数中有一个泛型T,方法中不能直接new T(),因为编译之后就是Object,不知道真实的类型。
4.静态方法无法引用类的泛型,因为类的泛型在实例化的时候才知道。
5.类型强转的额外开销。

泛型在特定场景可以通过反射获取,例如父类有一个泛型参数已经被确定,子类继承之后可以获取。例如gson中,解析带泛型的List,要传入一个TypeToken,实际上是new了一个子类,通过反射获取泛型类型。

如何写出线程安全的程序?

线程安全的本质,可变资源在线程间共享的问题。关键:可变资源、线程共享。

线程安全三要素:原子性、可见性、有序性

所以要保证线程安全:

1.共享不可变资源,final关键字的使用。
2.使用纯函数(不访问外部资源),使用ThreadLocal,不共享资源。
3.使用volatile关键字保证共享资源的可见性,并禁止指令重排序。
4.操作原子性(加锁保证操作的互斥性,原子类AtomicXXX的使用,CAS指令如Unsafe.compareAndSwap)

Synchronized原理

底层通过一个监视器monitor实现,monitor对象包含一个count计数字段和owner字段指向获取锁的线程,当线程获取monitor后,count+1,owner指向线程,监视器处于锁定状态,其他线程不能获取monitor会进入阻塞状态,当前线程释放monitor后,其他线程可以继续竞争该锁。

Java1.6之后对Synchronized进行了一些优化:

  • 锁自旋:线程的阻塞和唤醒需要 CPU 从用户态转为核心态,例如在Synchronized代码块中调用wait方法阻塞线程,wait会释放锁,所谓自旋,就是让该线程执行一段无意义的循环指令来等待一段时间,不会被立即挂起,看当前持有锁的线程是否会很快释放锁。缺点是需要占用 CPU,锁竞争的时间比较长时不实用)

  • 偏斜锁:如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作,一旦出现锁竞争,偏向锁会被撤销,并膨胀成轻量级锁

  • 轻量级锁:对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这把锁对象,也就是不存在锁竞争的情况。在这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作

Synchronized可以修饰静态方法(锁对象为字节码对象)、实例方法(锁为实例对象)和代码块,无论是否发生异常虚拟机都会正常释放锁
ReentrantLock发生异常时不能释放锁,所以一般需要在finaly代码块中释放锁,它包含公平锁和读写锁等用法,使用更灵活

java虚拟机内存模型

  • 虚拟机栈:线程私有,随线程创建而创建。栈里面是一个一个“栈帧”,每个栈帧对应一次方法调用。栈帧中存放了局部变量表(基本数据类型变量和对象引用)、操作数栈、方法出口等信息。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误。

  • 本地方法栈:线程私有,这部分主要与虚拟机用到的Native方法相关,一般情况下并不需要关心这部分内容。

  • 程序计数器:也叫PC寄存器,JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native方法,则PC寄存器中为空。(PS:线程执行过程中并不都是一口气执行完,有可能在一个CPU时钟周期内没有执行完,由于时间片用完了,所以不得不暂停执行,当下一次获得CPU资源时,通过程序计数器就知道该从什么地方开始执行)

  • 方法区:方法区存放类的信息(包括类的字节码,类的结构)、常量、静态变量等。字符串常量池就是在方法区中。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。很多人都更愿意把方法区称为“永久代”(Permanent Generation)。从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中。

  • 堆:堆中存放的是数组(PS:数组也是对象)和对象。当申请不到空间时会抛出OutOfMemoryError。

class加载过程

1.装载,将class文件加载进内存,在堆中生成class对象
2.链接,验证二进制数据流(类结构是否正确),分配静态变量设置默认值(初始化时才真正赋值),将符号引用转换为直接引用
3.初始化,初始化静态变量,静态代码块

java内存模型、volatile的作用

内存模型
  • 本地内存:存放的是 私有变量 和 主内存数据的副本。如果私有变量是基本数据类型,则直接存放在本地内存,如果是引用类型变量,存放的是引用,实际的数据存放在主内存。本地内存是不共享的,只有属于它的线程可以访问。

  • 主内存:存放的是共享的数据,所有线程都可以访问。当然它也有不少其他称呼,比如 堆内存,共享内存等等。

Java内存模型规定了所有对共享变量的读写操作都必须在本地内存中进行,需要先从主内存中拿到数据,复制到本地内存,然后在本地内存中对数据进行修改,再刷新回主内存,这就导致了多线程情况下数据的可见性问题,可以使用volatile关键字来修饰

  • volatile变量在修改后,会立即刷新主内存的值,对所有线程可见,当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程需要从主内存中去读取该变量的最新值。

  • volatile还可以防止指令重排序造成的线程安全问题,例如双重校验的懒汉式单例中,不加volatile的对象编译后的指令有可能重排序成:对象引用已经被赋值不等于null了,但是对象的构造方法还没有调用完成的情况。第二个线程去判断不为空,拿到的对象还未初始化完成造成错误。

如何安全停止一个线程

stop方法,被废弃。强行停止一个线程,没有资源的机会,如果正在处理任务,会留下一堆异常的数据。另一个线程再访问时就会发生错误。那么如何安全的结束呢:
1、设置volatile的boolean标志位,修改标志位来判断是否继续执行还是清理现场。

2、Interrupt方法:线程内部也需要做支持,判断是否被中断,和标志位类似的处理。支持sleep等系统方法(sleep过程中中断)。判断是否中断的两个方法的区别:
interrupted,静态方法,获取当前正在执行的线程是否被中断,中断之后会清空状态,重复获取就返回falseisInterrupted,线程的方法,获取当前线程的中断状态,不会被清空状态

HashMap原理

底层是数组+链表的结构,默认数组长度16,加载因子0.75,在put时,(如果第一次put,会创建数组)如果元素个数大于数组长度*加载因子时,将触发扩容操作,数组长度翻倍,并重新计算hash将元素放入数组;

Java1.8中,如果元素过多,数组长64,链表长度超过8,将进行树化操作,将链表转为红黑树,红黑树的节点是链表节点占用空间的两倍,提高查询效率;

如何计算元素存储的位置,如何解决hash冲突,为何数组长度必须为2的整数幂:

先把key取hash值,然后进行一个二次hash,方式为(n-1)&hash,这个二次hash是因为如果n正好等于2的幂,(n-1)&hash相当于对n取模,这样位运算效率很高,这样就相当于把元素均匀分布到了数组中,如果数组的位置没有元素,直接保存元素,如果已经有元素了,表示发生了hash冲突,将改为链表的存储方式,把新元素放在头部(1.8中是尾插法)

为什么加载因子为0.75?设为1和0.5有什么问题?

loadFactor太大,比如等于1,那么就会有很高的哈希冲突的概率,会大大降低查询速度。

loadFactor太小,比如等于0.5,那么频繁扩容没,就会大大浪费空间。

Hashtable

初始化容量不一样(11),线程安全对整个数组加锁,不允许null值,数据结构一直是数组+链表,不会转换为红黑树;

ConcurrentHashMap:

1.5-1.7采用分段锁segment机制,不再是整个数组加锁,而是对单条或者几条链表和红黑树进行加锁。内部结构如图:segment数组,segment中类似HashMap的数组+链表。要通过hash让元素尽可能的均匀分布到不同的segment和数组中,所以对key取hash,用高位确定segment的位置,然后用低位确定数组的位置。

1.5的hash算法不好,元素多的时候会造成新加的节点分布在最后的几个桶,分布不均匀,

1.6就改善了hash算法。

1.7的优化是采用segment的懒加载机制,并用volatile的方式访问数组,保证数组的线程可见性,结合CAS指令来避免加锁。

1.8中则基于hashmap做优化,不再采用分段锁,而是对桶节点加锁,使用volatile和CAS乐观锁来实现读和写,再次提高了效率。

通过对Hashtable和ConcurrentHashMap的比较,得出一些锁优化的方法论,比如大锁不如小锁,长锁不如短锁,读写锁的分离等等

线程池原理

线程池的参数

1.corePoolSize:线程池大小,当向线程池提交任务时,如果线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会创建一个新的线程来执行任务,直到线程数大于或等于corePoolSize。(除了提交新任务来创建线程,也可以通过prestartCoreThread或prestartAllCoreThreads来提前创建核心线程)
2.maximumPoolSize:线程池最大大小,当任务队列满了,且已创建的线程数小于最大线程数,则创建新线程来执行任务,如果线程池任务队列为无界队列可以忽略该参数
3.keepAliveTime:线程存活时间,当线程数大于核心线程数时,线程空闲时间大于存活时间,那么这个线程将被销毁,如果线程池任务队列为无界队列可以忽略该参数
4.workQueue:任务队列,用于保存等待执行任务的阻塞队列
5.threadFactory:线程工厂,用于创建新线程,可以设置统一风格的线程名
6.handler:线程饱和策略,当任务队列和线程池都满了,继续提交任务将执行此策略

如何配置线程池?需要看任务的类型

cpu密集型需要配置较小的线程数,避免cpu过度切换反而效率低下
IO密集型,线程池可以稍大,提高cpu的利用率;混合型任务则可配置两个线程池分别来执行;

java自带的线程池

线程池 核心线程 最大线程 存活时间 任务队列
CachedThreadPool 0 Integer.MAX_VALUE 60S SynchronousQueue
FixedThreadPool n n 0 LinkedBlockingQueue
SingleThreadExecutor 1 1 0 LinkedBlockingQueue
ScheduledThreadPool n Integer.MAX_VALUE 0 DelayWorkQueue

SynchronousQueue:只能有一个元素的队列,插入和获取元素都会阻塞线程

java方法分派(多态)

子类复写父类方法,调用方法调用子类还是父类? 取决于运行时具体的调用者类型,实例是子类就调用子类的方法。

HTTPS

  • 对称加密和非对称加密

  • 对称加密:加密和解密使用同一个秘钥,使用对应的加密和解密算法进行加解密

  • 非对称加密:加密和解密使用不同的秘钥,分为公钥和私钥,公钥和私钥相互可解,意思就是私钥加密的密文只有公钥可解,反之亦然。

  • 数字签名技术
    ​ 非对称加密在实际使用中,公钥会公开出来,私钥保存在自己手中不公开。由于私钥加密的密文只有公钥可解,那么如果有一个密文用你的公钥可以解开,那么可以说明这个密文肯定是你的私钥加密的,这就诞生了数字签名技术。
    ​ 只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明

https的本质

https的本质就是:用非对称加密的方式协商出一个对称加密的会话秘钥来进行会话

  • 首先服务端需要有一个证书,证书包含了自己的公钥和服务端信息,如hash算法、加密算法、域名、有效期等等。此证书需要由可信任的第三方(CA机构)的私钥进行签名,实际上是对证书做一个hash,得到hash值然后签名,CA机构也可能不止一级而是一个证书链
  • 为什么要用第三方机构来颁发证书呢?为了安全的传输自己的公钥,系统都预置了可信任的根证书,三方机构是否可信任由系统来保证

客户端如何校验CA证书

1.客户端收到证书后,用证书中的公钥去解密该Hash值,得到hash-a
2.客户端用证书中指定的签名算法,计算出一个hash-b,比较hash-a和hash-b
3.除了校验hash值,还会校验CA证书有效期和域名等

SSL握手过程

1.客户端A访问服务端B,客户端生成一个随机数1、将自己支持的SSL版本号、加密套件(包括哈希算法和加密算法)等信息发送给服务端
2.服务端B收到请求,选择一个加密套件,也生成一个随机数2,将随机数和自己的证书一同返回给客户端
3.客户端收到证书,校验证书是否有效(方法之前说过了),通过校验后,生成一个随机数3,用证书中的公钥加密随机数3,发送给服务端B
4.服务端收到加密的随机数,用私钥解密
5.服务端和客户端都有了随机数1、2、3,通过这三个随机数,生成一个对称加密的会话密钥
6.服务端和客户端分别通知对方之后的会话用会话秘钥来完成,握手结束

为什么要用非对称加密来握手,而用对称加密来会话

对称加密握手的话,由于双方的秘钥是一样的,相当于秘钥公开了,和没加密没有区别
而会话阶段,对称加密效率较非对称高

TCP为什么要三次握手和四次挥手

  • “三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。例如:第一次请求由于网络拥堵没有到达服务端,客户端又发起第二次请求,正常完成了连接,传输完数据之后断开,这时第一次的请求到达了服务端,如果没有第三次握手,会直接建立一条没有用的连接,server端一直等待,浪费资源。

+“四次挥手”原因是因为tcp是全双工模式,接收到FIN时意味对方将没有数据再发来,但是自己还是可以继续发送数据。

为什么TCP是可靠的?

TCP基于连接,具有以下机制:

  • 确认和重传:接收方收到报文后就会进行确认,发送方一段时间没有收到确认就会重传。
  • 数据校验。
  • 数据合理分片与排序,TCP会对数据进行分片,接收方会缓存为按序到达的数据,重新排序后再提交给应用层。
  • 流程控制:当接收方来不及接收发送的数据时,则会提示发送方降低发送的速度,防止包丢失。
  • 拥塞控制:当网络发生拥塞时,减少数据的发送。

UDP是无连接、不安全的,每个数据包都包含接收的ip等信息,客户端只管发送,没有确认重传机制,所以速度更快,但是可能会丢包。

HTTP1.0、1.1、2.0的区别

1.1和1.0:
  • 增加新的控制缓存策略的Header,如Entity tag,If-Unmodified-Since, If-Match, If-None-Match;

  • 增加了range请求头,允许请求资源的一部分,支持了多线程断点续传下载,优化了带宽和连接;

  • 增加了Host头,允许一台物理服务器上存在多个虚拟主机,共享一个IP地址,通过Host来区分;

  • 增加了keep-alive支持TCP长连接,一定程度弥补了每次请求都重新创建连接的情况;

SPDY:

SPDY是Http1.x版本的优化方案,包括多路复用技术、请求优先级(多路复用时,多个请求并行于共用的TCP连接,可以设置请求的优先级防止关键请求被阻塞)、header压缩和服务端推送功能;SPDY的特性并入了Http2.0中;

1.1和2.0:

  • 支持了新的二进制格式,1.x版本只支持文本协议

  • 多路复用技术,在HTTP/1.1协议中,同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。多个请求是串行处理,当一个请求超时,后续请求就会被阻塞,而在2.0中,一个TCP连接上并行多个请求,某个请求耗时不影响其他连接;

  • Header压缩,多个请求可以差量更新Header字段,降低流量提高效率;

  • 服务端推送功能

三方授权方式

  • Basic:格式:Authorization: Basic username:password(Base64ed)

  • Bearer:格式:Authorization: Bearer

bearer token 的获取⽅式( OAuth2 的授权流程):

1.第三⽅⽹站向授权⽅⽹站申请第三⽅授权合作,拿到 client id 和 client secret

2.⽤户在使⽤第三⽅⽹站时,点击「通过 XX (如 GitHub) 授权」按钮,第三⽅⽹站将⻚⾯跳转到授权⽅⽹站,并传⼊ client id 作为⾃⼰的身份标识

3.授权⽅⽹站根据 client id ,将第三⽅⽹站的信息和需要的⽤户权限展示给⽤户,询问⽤户是否同意授权

4.⽤户点击「同意授权」按钮后,授权⽅⽹站将⻚⾯跳转回第三⽅⽹站,并传⼊ Authorization code 作为⽤户认可的凭证。

5.第三⽅⽹站将 Authorization code 发送回⾃⼰的服务器

6.服务器将 Authorization code 和 client secret ⼀并发送给授权⽅的服务器,授权⽅返回 access token。

WebSocket和Socket的区别

  • WebSocket是应用层的一个持久化协议,http它一次请求和响应就断开连接,属于非持久化协议。WebSocket分为握手和数据传输两个阶段,采用http协议握手然后建立全双工的tcp连接。
  • Socket是传输层的一个协议抽象,包括TCP和UDP,TCP基于连接,拥有确认和重传,拥塞控制和流程控制等机制的可靠的协议。UDP则面向无连接,基于数据报,相对于TCP速度快但不可靠。

多线程下载和断点续传

两个核心Header,Content-Length表示文件的总字节数,RANGE表示从某一个位置开始传输。

首先,获取到文件大小后,通过线程数来计算每个线程下载的开始位置。

然后,通过range来设置从哪个位置传输。

当暂停或者退出时,记录已下载的位置,下次恢复后从记录的位置下载。

使用RandAccessFile来保存文件,这个类的特点是可以通过移动文件指针来设置写入的位置。

offce 003

Retrofit

Retrofit的主要原理是什么?

  • 回答: Retrofit是一个基于注解和反射的HTTP客户端库,主要原理是通过创建一个接口描述HTTP请求,使用注解标记请求的参数和返回值,并通过动态代理生成实现该接口的具体实例。在运行时,Retrofit会使用反射机制解析接口的注解,构建和执行HTTP请求,然后将响应转化为Java对象。它通常与OkHttp一起使用,利用OkHttp的强大功能来处理网络请求。

OkHttp3

OkHttp3的主要原理是什么?

  • 回答: OkHttp3是一个高效的HTTP客户端库,主要原理是建立在连接池、拦截器和异步处理的基础上。它通过连接池重用HTTP/HTTPS连接,减少了网络请求的延迟;拦截器允许在请求和响应的处理过程中插入自定义逻辑;异步处理则通过回调或者返回Call对象来处理非阻塞的网络请求。OkHttp3还支持请求和响应的压缩、缓存、HTTPS等高级特性。

Dagger 2

Dagger 2的主要原理是什么?

  • 回答: Dagger 2是一个依赖注入框架,主要原理是通过编译时生成的代码来实现依赖注入。它使用注解和代码生成技术,通过解析和分析依赖关系,在编译时生成对应的依赖注入代码,以提高性能和减少运行时的性能开销。Dagger 2的注入过程是在编译时确定的,这保证了类型安全,并且生成的代码是高效的。

ARouter

ARouter的主要原理是什么?

  • 回答: ARouter是一个Android平台的路由框架,主要原理是基于注解和编译时处理器。在使用ARouter时,通过在Activity、Service等组件上添加注解标记路由路径,然后在编译时通过注解处理器生成对应的路由表。在运行时,ARouter根据路由表进行路由匹配,找到对应的组件,并执行相应的跳转逻辑。ARouter还支持参数传递、拦截器等功能,使得在Android应用中实现页面跳转更加方便和灵活。
    这些框架的原理都涉及到了编译时注解处理、反射、动态代理等技术,了解这些原理有助于更好地使用这些框架并理解它们的运行机制。在面试中,除了回答原理,还可以结合实际项目经验,讨论在项目中如何使用这些框架以及遇到的问题和解决方案。

kotlin flow 和 livedata

Flow 和 LiveData 都是用于处理异步操作、响应式编程以及在 Android 中处理 UI 事件的工具。它们之间有一些区别,下面是它们的主要特点和用法:

Flow

1.异步流:

  • Flow 是 Kotlin 中的一种冷流(Cold Flow),支持异步操作。它提供了一种声明式的方式来处理异步事件序列,允许使用 suspend 函数来执行异步操作。

2.协程中使用:

  • Flow 通常与协程一起使用,可以在协程中使用 flow { … } 构建一个流,然后使用 collect 在协程中收集流的数据。

3.背压支持:

  • Flow 支持背压(Back Pressure),这意味着可以通过 buffer、conflate、collectLatest 等操作符来控制数据流的速率,以避免内存溢出等问题。

4.线程调度:

  • 通过 flowOn 操作符,可以在流的操作链中切换线程,实现线程的调度。

5.异常处理:

  • Flow 提供了异常处理机制,可以通过 catch 操作符捕获流中的异常。

LiveData

1.生命周期感知:

  • LiveData 是 Android 架构组件,具有生命周期感知能力。它会自动在活跃生命周期状态下触发 UI 更新,防止内存泄漏。

2.主线程操作:

  • LiveData 主要用于在主线程中更新 UI,因此适合与 Android 的 UI 组件结合使用。

3.数据观察:

  • 通过 observe 方法,可以在活跃生命周期状态下观察数据的变化,并在数据变化时触发相应的操作。

4.不支持背压:

  • LiveData 不支持背压,因此在大量数据产生时需要注意,以防止因数据过多而引起内存问题。

5.单向数据流:

  • LiveData 主要是单向数据流,用于从数据源向 UI 推送数据。在架构中,通常与 ViewModel 结合使用。

选择使用场景:

  • 使用 Flow 当你需要处理更为复杂的异步操作、需要支持背压、或者希望与协程更为紧密地结合使用。

  • 使用 LiveData 当你希望数据能够自动在主线程中更新 UI,或者在使用 Android 架构组件时。

在实际应用中,Flow 和 LiveData 可以一起使用,通过 flow.asLiveData() 将流转换为 LiveData,从而实现在协程和 UI 生命周期中的无缝衔接。这样可以在需要强调响应式编程时使用 Flow,而在需要与 Android UI 结合使用时使用 LiveData。

协程

1.什么是协程?
解答: 协程是一种轻量级的线程设计模式,用于处理异步任务和并发编程。在Kotlin中,协程通过suspend关键字和CoroutineScope提供了一种更简洁和可控的异步编程方式。

2.协程与线程的区别是什么?
解答: 协程是更轻量、更高效的并发编程方式,不会创建新的系统线程,而是在现有的线程上挂起和恢复。相比之下,线程涉及更多的系统资源和上下文切换开销。

3.协程的主要组件是什么?
解答: 协程的主要组件包括协程构建器(如launch和async)、suspend函数用于挂起协程的执行、以及CoroutineScope用于管理协程的生命周期。

4.如何在Android中使用协程?
解答: 在Android中使用协程,首先需要在项目中引入Kotlin协程库(implementation “org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0”),然后在协程的作用域中使用launch或async等协程构建器。

5.如何处理协程中的异常?
解答: 可以使用try/catch块来捕获协程中的异常。另外,可以使用CoroutineExceptionHandler来全局处理协程中未捕获的异常。

6.协程中的取消是如何工作的?
解答: 取消协程可以通过Job对象的cancel方法来实现。协程内部通过检查协程的取消状态来判断是否应该终止执行。

7.如何实现协程的延迟执行?
解答: 可以使用delay函数来实现协程的延迟执行。例如,delay(1000)会使协程挂起1秒钟。

8.什么是协程的上下文(CoroutineContext)?
解答: 协程的上下文包含了协程的各种配置和元素,例如调度器、异常处理器等。协程通过CoroutineScope的上下文来继承和配置。

9.如何在协程中执行异步任务?
解答: 可以使用async函数来执行异步任务,它返回一个Deferred对象,可以通过await函数等待异步任务的完成。另外,也可以使用withContext切换线程执行异步任务。

10.什么是挂起函数(suspend function)?
解答: 挂起函数是用suspend关键字标记的函数,它可以在协程中被挂起,而不会阻塞线程。挂起函数可以包含挂起点,例如调用delay、await等。
这些问题涵盖了协程的基本概念、使用方法以及一些高级特性。在面试中,还可能根据具体项目经验和问题场景进行更深入的提问。

MVVM(Model-View-ViewModel)面试题目与解答:

1.什么是MVVM模式?
答案: MVVM是一种软件设计模式,将应用程序划分为三个主要组件:Model(数据和业务逻辑)、View(用户界面)、ViewModel(连接Model和View,处理用户输入和业务逻辑)。

2.MVVM与MVC的区别是什么?
答案: MVVM与MVC相似,但在MVVM中,ViewModel负责处理用户输入和业务逻辑,将视图和模型解耦。这使得UI和业务逻辑更容易测试和维护。

3.在Android中,ViewModel的作用是什么?
答案: 在Android中,ViewModel主要负责管理UI相关的数据和业务逻辑。它的生命周期与Activity或Fragment相关联,可以在设备配置更改(如旋转屏幕)时保持数据的一致性。

4.LiveData在MVVM中的作用是什么?
答案: LiveData是一种可观察的数据持有类,与生命周期相关联。在MVVM中,ViewModel通常使用LiveData来存储和通知UI相关的数据变化,以确保数据的同步更新。

5.如何在Android中实现双向数据绑定?
答案: 在Android中,可以使用Data Binding库实现双向数据绑定。这使得ViewModel中的数据变化能够自动更新到UI,并且UI上用户的输入也能够反馈到ViewModel中。

6.解释一下Data Binding库的作用。
答案: Data Binding库允许开发者将布局文件与应用程序的数据绑定在一起,减少了手动编写界面代码的需求。它可以使布局文件中的视图自动更新,也支持双向数据绑定,简化了UI和数据之间的同步。

7。什么是Repository模式在MVVM中的作用?
答案: Repository模式用于抽象数据源的实现,使ViewModel与底层数据层解耦。它可以从本地数据库或远程服务器获取数据,并将数据提供给ViewModel。这有助于保持代码的清晰性和可维护性。

8.如何处理在ViewModel中进行异步操作?
答案: 通常,可以使用协程(Kotlin Coroutine)或RxJava等工具在ViewModel中进行异步操作。这有助于在不阻塞UI线程的情况下执行长时间运行的任务,并且能够更容易地处理异步结果。

Android ViewModel

1.什么是ViewModel?
解答: ViewModel是Android架构组件的一部分,用于存储和管理与UI相关的数据。它的生命周期与Activity或Fragment相关联,可在屏幕旋转等配置更改时保持数据的一致性。

2.ViewModel的作用是什么?
解答: ViewModel的主要作用是在配置更改(如屏幕旋转)时保留数据,并在Activity或Fragment重建时提供可用的数据。它有助于分离UI控制器与数据逻辑,提高应用的可维护性。

3.ViewModel与SavedInstanceState的区别是什么?
解答: ViewModel主要用于存储与UI相关的数据,并在配置更改时保持一致性,而SavedInstanceState用于存储少量的基本数据,例如EditText的内容,以便在Activity重建时恢复。

4.如何在ViewModel中处理异步操作?
解答: 可以使用ViewModelScope结合协程来处理异步操作。在ViewModel中使用viewModelScope.launch启动协程,执行异步任务,确保在ViewModel的生命周期内正确处理异步操作。

5.ViewModel如何避免内存泄漏?
解答: ViewModel通过与Activity或Fragment相关联,当它们被销毁时,ViewModel也会被销毁,避免了内存泄漏。确保不直接持有对Activity或Fragment的引用,以免导致无法被垃圾回收。

6.如何在ViewModel中共享数据?
解答: 可以使用LiveData或StateFlow作为ViewModel中数据的容器,以便在数据变化时通知观察者。这样,在多个组件中观察相同的LiveData或StateFlow,就能实现数据的共享。

7.ViewModel的生命周期是怎样的?
解答: ViewModel的生命周期由相关联的Activity或Fragment的生命周期决定。当Activity或Fragment销毁时,ViewModel也会被销毁。在屏幕旋转等配置更改时,ViewModel会被保留。

8.ViewModel和Repository的关系是什么?
解答: ViewModel通常与Repository一起使用,ViewModel负责管理UI相关的数据和业务逻辑,而Repository负责获取和管理数据。这样ViewModel与数据层解耦,使得数据获取和处理更为清晰和可维护。

9.如何在ViewModel中使用Dagger(或其他依赖注入框架)?
解答: 可以在ViewModel中使用Dagger,通过Dagger提供的ViewModelFactory或ViewModelProvider.Factory来创建ViewModel实例。这样能够将依赖注入到ViewModel中,使其更具可测试性和可维护性。

10.ViewModel的构造函数可以接收参数吗?
解答: 在普通的ViewModel中,构造函数不能接收参数,因为系统会通过反射来创建ViewModel实例。但是,可以使用ViewModelProvider.Factory来创建接收参数的ViewModel,以便在创建时传递参数。
这些问题涵盖了ViewModel的基本概念、用法以及一些与之相关的主题。在面试中,还可能根据项目经验和具体需求提问更深入的问题。

线程阻塞主要什么原因

线程阻塞是指线程无法继续执行下去,暂时停止执行直到某个条件满足。主要的原因包括:

1.I/O操作: 当线程进行输入/输出操作,例如读写文件、网络通信时,由于这些操作是相对较慢的,线程可能会被阻塞,直到I/O操作完成。

2.等待锁: 在多线程编程中,线程可能会等待获取某个对象的锁。如果其他线程已经持有该锁,当前线程将被阻塞,直到锁可用。

3.调用sleep()方法: 使用Thread.sleep()方法可以让线程休眠一段时间,这会导致线程暂时阻塞。

4.等待其他线程完成: 在一些情况下,一个线程可能需要等待其他线程完成某个任务,这也会导致线程阻塞。

5.等待通知: 使用Object.wait()方法,线程可能会进入等待状态,直到其他线程调用相同对象上的Object.notify()或Object.notifyAll()方法唤醒它。

6.条件不满足: 线程在执行过程中,如果某个条件不满足,可能会主动或被动地阻塞,等待条件满足后继续执行。

7.死锁: 死锁是多个线程相互持有对方所需的资源,而无法继续执行的状态。这种情况下,所有涉及的线程都会被阻塞。

8.等待外部事件: 线程可能会等待外部事件的发生,例如等待用户输入或等待某个信号。

线程阻塞的原因多种多样,而解决方法通常包括使用合适的同步机制、使用异步编程、优化I/O操作、避免死锁等手段。在多线程编程中,避免不必要的阻塞,合理设计线程同步和通信机制是至关重要的。

1.什么是主线程(UI线程)和后台线程?
答案: 主线程是应用程序的主要执行线程,负责处理用户界面和响应用户输入。后台线程用于执行耗时操作,以避免阻塞主线程,例如网络请求、数据库查询等。

2.Android中常见的多线程实现方式有哪些?
答案: 常见的多线程实现方式包括继承Thread类、实现Runnable接口、使用Handler、AsyncTask、IntentService、以及使用线程池(ThreadPoolExecutor)等。

3.什么是ANR?如何避免ANR?
答案: ANR(Application Not Responding)是指应用程序在主线程上执行耗时操作,导致用户界面无法响应。为避免ANR,应将耗时操作放在后台线程执行,例如使用AsyncTask或Handler来异步处理任务。

4.什么是AsyncTask?它的使用场景是什么?
答案: AsyncTask是Android提供的一个简化多线程编程的工具类,用于在后台线程执行异步任务,并在主线程更新UI。它适用于短时间的异步操作,例如网络请求、数据库查询等。但对于长时间运行的任务,建议使用其他更灵活的方法,如使用ThreadPoolExecutor。

5.什么是Handler和Looper?它们的作用是什么?
答案: Handler和Looper用于实现线程间的通信。Looper负责管理线程的消息队列,而Handler用于向消息队列中发送消息和处理消息。在Android中,主线程的消息队列由系统自动创建,而后台线程需要显式创建Looper和Handler。

6.如何在后台线程中更新UI?
答案: 可以使用Handler,在后台线程中通过Handler向主线程发送消息,然后在主线程中处理消息,更新UI。另一种方法是使用AsyncTask,在后台线程执行任务后,通过onPostExecute方法回到主线程更新UI。

7.什么是线程安全?如何保证线程安全?
答案: 线程安全是指多个线程同时访问共享资源时,不会出现数据不一致的情况。可以通过使用锁(synchronized关键字)、使用线程安全的集合类、使用volatile关键字、以及使用原子操作等方式来保证线程安全。

8.什么是线程池?为什么使用线程池?
答案: 线程池是管理和复用线程的机制,它可以避免频繁创建和销毁线程带来的开销。通过线程池,可以有效地控制并发线程数量,提高系统性能。在Android中,可以使用ThreadPoolExecutor或者Executors工具类创建线程池。

9.什么是Deadlock(死锁)?如何避免死锁?
答案: 死锁是指两个或多个线程互相等待对方释放资源,导致程序无法继续执行。为避免死锁,可以使用加锁的顺序保持一致,避免一个线程持有一个锁的同时等待另一个锁。此外,可以使用tryLock替代传统的synchronized关键字,以避免死锁的发生。

七友

梁汉文 - 七友
曲 : 雷颂德 词:林夕

为了她又再勉强去谈天论爱
又再振作去慰解他人
如难复合便尽早放开
凡事看开
又再讲没有情人时还可自爱
忘掉或是为自己感慨
笑住说沉沦那些苦海
会有害

因为我坚强到利用自己的痛心
转换成爱心
抵我对她操心
已记不起我也有权利爱人

谁人曾照顾过我的感受
待我温柔吻过我伤口
能得到的安慰是失恋者得救后
很感激忠诚的狗
谁人曾介意我也不好受
为我出头碰过我的手
重生者走得的都走
谁人又为天使忧愁
甜言蜜语没有但却有我这个好友

直到她又再告诉我重新被爱
又再看透了我的将来
完成任务后大可喝采
无谓搭台
别怪她就怪我永远难得被爱
然后自虐地赞她可爱
往日最彷徨那刻
好彩有我在

因为我坚强到利用自己的痛心
转换成爱心
抵我对她操心
已记不起我也有权利爱人

谁人曾照顾过我的感受
待我温柔吻过我伤口
能得到的安慰是失恋者得救后
很感激忠诚的狗
谁人曾介意我也不好受
为我出头碰过我的手
重生者走得的都走
谁人又为天使忧愁
甜言蜜语没有但却有我这个好友

白雪公主不多
认命扮矮人的有太多个
早有六个
多我这个不多
我太好心还是太傻
未问过她有没有理我的感受
待我温柔吻过我伤口
能得到的安慰是失恋者得救后
很感激忠诚的狗
谁人曾介意我也不好受
为我出头碰过我的手
重生者走得的都走
谁人又为天使忧愁
甜言蜜语没有但却有我这个好友

gradle目录解析

Gradle目录解析

Gradle 是以 Groovy 语言为基础,面向Java应用为主。基于DSL(领域特定语言)语法的自动化构建工具。
Gradle这个工具集成了构建,测试,发布和其他,比如软件打包,生成注释文档等功能。
之前eclipse使用ant进行软件的构建功能,需要配置一大堆的xml,但是在gradle中就不需要了。

目前主流的打包方式有ant,maven,gradle。gradle是近几年发展起来的自动化构建工具,解决ant构建上的繁琐代码。
比如在ant上发布多渠道的包,你需要自己写脚本替换渠道名称,而在gradle中就不需要了。已经内建支持多渠道打包。

Gradle的文件结构

./build.gradle
./gradle.properties
./gradlew
./gradlew.bat
./local.properties
./setting.gradle
./XXX.iml
./app/build.gradle
./app/app.iml
./app/proguard-rules.pro

./builld.gradle 和 ./app/build.grade

gradle项目自动编译的时候要读取的配置文件。比如指定项目的依赖包等。
build.grade有两个,一个是全局的,一个是在模块里面。
全局的build.grade主要设置的是声明仓库源,gradle的版本号说明等。

./build.gradle

buildscript {
repositories {
    // 声明仓库源,比如我们构建了一个安卓的库,现在想要把库上传到jcenter中供别人一起使用,则可以上传到jcenter中
    // 具体上传步骤见:http://www.jcodecraeer.com/a/anzhuokaifa/Android_Studio/2015/0227/2502.html
    jcenter()
}
dependencies {
    // 说明gradle的版本号
    classpath 'com.android.tools.build:gradle:1.3.0'
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}
}

// 所有项目都继承这个配置

    allprojects {
    repositories {
        mavenLocal()
        jcenter()
    }
}

./app/build.grade 设置了模块的gradle构建配置

// 说明这个模块是安卓项目,如果是多模块开发,有可能有的值为java/war

apply plugin: ‘com.android.application’

// 配置了所有android构建的参数
android {
// 编译使用SDK版本
compileSdkVersion 23
// 编译工具的版本
buildToolsVersion “23.0.1”

defaultConfig {
    // 包名
    applicationId "com.awesomeproject"
    // sdk最低支持版本
    minSdkVersion 16
    // 目标SDK版本,如果目标设备的API版本正好等于此数值,就不会为此程序开启兼容性检查判断的工作
    targetSdkVersion 22
    // 版本号
    versionCode 1
    versionName "1.0"
    // 原生
    ndk {
        abiFilters "armeabi-v7a", "x86"
    }
}
buildTypes {
    // 发布时候的设置
    release {
        // 是否进行混淆
        minifyEnabled false
        // 混淆使用文件
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
}
// 依赖的工具包
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.0.0'
compile 'com.facebook.react:react-native:0.11.+'
}

./app/proguard-rules.pro
这个和上面说的一样混淆文件

./gradle.properties
grade的运行环境配置,比如使用多少内存之类的。

./gradlew 和 ./gradlew.bat
自动完成 gradle 环境的脚本,在linux和mac下直接运行gradlew会自动完成gradle环境的搭建。

./local.properties
配置SDK或者NDK的环境路径,各个机器上这个变量可能都是不一样的,所以不应该进入版本库

./setting.gradle
整个项目的管理,比如这个项目包含哪些模块等。

./XXX.iml 和 ./app/app.iml
iml是Intellij模块文件。Intellij是一款JAVA的IDE。AndroidStudio是基于开源的Intellij IDEA开发出来的IDE。所以AndroidStudio有的IDE功能是需要有.iml才能使用的。比如我们删除了iml文件,可能就在Android Studio中看不到一些目录了。