0%

进程和线程

进程是操作系统资源分配的最小单位,线程是 CPU 调度的最小单位。

一个标准的线程主要由3部分构成:线程描述信息(线程ID、线程名称、线程状态、线程优先级、其他)、程序计数器 和 栈内存。其中,程序计数器它记录着线程下一条指令的代码段内存地址;每个线程在创建的时候默认被分配 1M 大小的栈内存大小,和堆内存不同,栈内存不受垃圾回收器管理

当线程执行流程进入方法时,JVM 就会为方法分配一个对应的栈帧压入栈内存;当线程流程跳出方法时,JVM 就从栈内存弹出该方法的栈帧,此时方法帧的局部变量的内存空间就会被回收。

创建线程的4种方法

Thread 类中有个属性, private boolean deamon = false; 该属性保存 Thread 线程实例的守护状态,默认为false ,表示普通用户线程,而不是守护线程。 守护线程是在进程运行提供某种后台服务的线程,比如GC 线程。

  • 继承Thead 类创建线程类(Thread 本身就实现了 Runnable )

  • 实现 Runnable 接口创建线程目标类(传入 Thread 中时,Thread将其以 target 字段保存)

  • 使用 Callable 和 FutureTask 创建线程

  • 通过线程池创建

前面2种创建方法有一个共同的缺陷:不能获取异步执行的结果。在此基础上,才有了使用 Callable 和 FutureTask 创建线程的方法。

Callable 接口

Callable 是个泛型接口,它的源码如下:

1
2
3
public interface Callable<V> {
V call() throws Exception;
}

Callable 接口类似与 Runnable ,不同的是 Runnable 的run 方法没有返回值,也没有接收异常的异常声明,因此,感觉 Callable 更强大一些,只是 Callable 暂时还没能和 Thread 产生关系。不过,一个在 Callable 接口与 Thread 线程之间搭桥的重要接口 RunnableFuture 接口就要登场了。

RunnableFuture 接口

RunnableFuture 接口与 Runnable 接口、Thread 类紧密相关,源码如下:

1
2
3
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}

可以看出,它继承了 Runnable 接口,从而可以作为 Thread 的 target ,同时还继承了 Future 接口,保证了可以获取未来的异步执行结果。

Future 接口

Future 接口至少提供了三大功能:

  • 能够取消异步执行中的任务

  • 判断异步任务是否执行完成

  • 获取有任务完成后的执行结果

它的源码如下:

1
2
3
4
5
6
public interface Future<V> {
boolean isCancelled();
boolean isDone();
V get();
V get(long timeout, TimeUnit unit) throws InterruptedException
}

虽然它提供了这么多功能,但它终归只是个接口,通过它没法直接完成对异步任务的操作,为此,JDK 提供了一个默认实现类: FutureTask 。

FutureTask 类

FutureTask类实现了RunnableFuture接口,相当于既实现了 Runnable 接口,又实现了 Future 接口。所以FutureTask既能作为一个 Runnable 类型的 target 被Thread执行,又能作为Future异步任务来获取Callable的计算结果。

使用Callable和FutureTask创建线程的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String args[]) throws InterruptedException, ExecutionException {
FutureTask<Long> futureTask = new FutureTask<Long>(new Callable<Long>() {

@Override
public Long call() throws Exception {
long startTime = System.currentTimeMillis();
Thread.sleep(1000);
return System.currentTimeMillis() - startTime;
}
});
Thread thread = new Thread(futureTask, "returnableThread");
thread.start();

System.out.println(thread.getName() + "线程占用时间:" + futureTask.get());
}

returnableThread线程首先执行 thread.run() 方法,然后在其中执行其target(futureTask任务)的run()方法;接着在futureTask.run()方法中会执行 callable 成员的 call()方法。Callable的call()方法执行完成后,会将结果保存在FutureTask内部的outcome实例属性中。这里有两种情况:

  • futureTask的结果outcome不为空,callable.call()执行完成,futureTast.get会直接取回outcome结果返回给结果获取线程。

  • futureTask的结果outcome为空,callable.call()还没有执行完。则结果获取线程会被阻塞住直到callable.call()执行完成。当执行完后,最终结果会保存到outcome中,futureTask会唤醒结果获取线程。

1.4 线程的核心原理

1.4.1 线程的调度与时间片

在不同的操作系统、不同的CPU上,线程的 CPU 时间片长度都不同,windows xp 的时间片长度为 20 毫秒,线程调度模型主要分为 2 种:

  • 分时调度。系统平均分配 CPU 的时间片,人人平等

  • 抢占式调度。按照线程的优先级分配时间片,如果大家优先级都相同,就随机选择一个,目前大部分操作系统选择的是这种调度方式

1.4.2 线程优先级

对于优先级,有以下结论:

  • 整体而言,高优先级的线程获得的执行机会更多

  • 执行机会的获取具有随机性,优先级高的不一定获得机会多(文中的例子10级的优先级比9级的优先级获得的机会还少,个人认为可能的一个因素是:Java的线程映射到系统线程时,10级与 9 级并没有区分)

1.4.3 线程的生命周期

Java 的线程有6种状态:

  • NEW: 新建,创建成功,但是没有调用 start() 方法

  • RUNNABLE:可执行,包含操作系统的就绪、运行2种状态

  • BLOCKED:阻塞

  • WAITTING:等待

  • TIMED_WAITTING:限时等待,包括Thread.sleep(n)、Object.wait(n)、Thread.join(n)、LockSupport.parkNanos(n)、LockSupport.parkUntil(n) 等

  • TERMINATED:终止。线程的 run 方法执行完毕(或者执行run方法被异常终止)

1.4.5 使用 Jstack 工具查看线程状态

Jstack 是Java 虚拟机自带的 堆栈跟踪工具,用于生成或者导出(Dump)JVM 运行实例当前时刻的线程快照。命令的语法格式如下:

jstack //pid表示 Java 进程的 id,可以用 jps 命令查看

1.5 线程的基本操作

1.5.2 线程的sleep 操作

sleep 的作用是让线程休眠,让 CPU 执行其他的任务,从状态来讲就是从执行状态变成限时阻塞状态。当睡眠时间满后,线程不一定立即得到执行,因为此时CPU可能正在执行其他任务,所以还需要等待分配时间片。

1.5.3 线程的 interrupt 操作

Java 语言提供了 stop() 方法终止正在运行的方法,但后来不建议使用了,因为这像突然关闭计算机电源一样,无法知道这个线程在处于什么状态,它可能持有某把锁,强行停止可能会导致锁无法释放;或者可能正在操作数据库,强行停止导致数据不一致。

一个线程什么时候可以退出只有线程自己直到,所以,这里介绍的 interrupt 方法本质不是用来中断一个线程,而是将线程设置为中断状态。当我们调用线程的 interrupt 方法时,它有2个作用:

  • 如果此线程处于阻塞状态,就会立马退出阻塞,并抛出 InterruptedException 异常,线程可以通过捕获 InterruptedException 异常来做一定处理,从而提早终结被阻塞状态。

  • 如果线程正在运行,就不受任何影响,仅仅只是中断标记被置为 true 了。

如果 interrupt()方法先被调用,然后线程开始调用阻塞方

法进入阻塞状态,InterruptedException异常依旧会抛出;如果线程

捕获InterruptedException异常后,继续调用阻塞方法,将不再触发

InterruptedException异常。

1.5.4 线程的join操作

也就是线程的合并操作,可以用一个例子来说明: 假设线程 A 和 B,现在 A 执行过程中对 B 的执行有依赖:A 需要将 B 的执行流程合并到自己执行的流程中,这就是线程合并,伪代码大概如下:

1
2
3
4
5
6
class ThreadA extends Thread {
public void run() {
Thread threadb = new Thread("thread-b");
threadb.join();
}
}

上述代码中,执行 threadb.join() 这行代码的当前线程为合并线程(线程 A)会进入 TIMED_WAITING 等待状态,让出 CPU。所以上述过程应该是等 B线程执行完成之后,A 线程再继续执行

1.5.5 线程的 yield 操作

线程的 yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出 CPU执行权限,之后变为 RUNNABLE 状态,从操作系统层面来讲就是进入了就绪状态(而不是阻塞)。在线程 yield 时,线程放弃和重占 CPU 的时间是不确定的,可能刚放弃了 CPU ,马上又获得了 CPU 的执行权限,接着开始执行了。

1.5.6 线程的daemon操作

守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行一系列的 GC 线程,这些 GC 线程就是守护线程。

只要 JVM 中尚存任何一个用户线程没有结束,守护线程就能执行自己的工作,只有当最后一个用户线程结束,守护线程随同 JVM 一同结束工作。

守护线程的要点

使用守护线程时,有几点需要特别注意:

  • 线程必须在start() 方法调用前设置其为守护线程(即调用setDaemon(true)方法) ,否则会抛出 InterruptedException异常

  • 在守护线程中创建的线程,新的线程都是守护线程。如果要创建用户线程,需要显式调用 setDaemon(false)

1.6 线程池原理与实战

Java 线程的创建非常昂贵,需要 JVM 和 OS 配合完成大量工作:

  • 为线程堆栈分配大量内存快,包括至少 1M 的栈内存
  • 需要进行系统调用,以便在OS中创建和注册本地线程

而线程池的出现主要解决了以下问题:

  • 提升性能:最大限度地复用已经创建的线程,避免创建和销毁,提升性能

  • 线程管理:线程池可以保持对线程的统计信息,例如完成任务数量、空闲时间等,能对异步任务进行高效调度(我个人觉得还有可能根据cpu的核心数之类的确定线程数量)。

1.6.1 JUC 的线程池架构

JUC 是 java.util.concrrent 工具包的简称,是用于完成高并发、处理多线程的一个工具包。其中的线程池类与接口的架构图大致如下:

线程池类与接口的架构图

1.6.2 Executors 的4种快捷创建线程池的方法

Java 通过 Excutors 工厂类提供了4种快捷创建线程池的方法,如下表所示:

方法名 功能简介
newSingleThreadExcutor 创建只有一个线程的线程池
newFixedThreadPool 创建固定大小的线程池
newCachedThreadPool 创建一个不限制线程数量的线程池,任何提交的任务都立即执行,但是空闲线程会得到及时回收
newScheduledThreadPool 创建一个可定期或者延时执行的任务的线程池

总的来说,newSingleThreadExcutor 线程池有以下特点:

  • 线程池中的任务是按照提交的顺序执行的

  • 池中的唯一线程的存活时间是无限的

  • 当池中唯一线程繁忙时,新提交的任务会进入内部的阻塞队列,这个队列是无界队列

线程池使用完后,要调用 shutDown 来关闭线程池,此时线程池将变为 SHUTDOWN 状态,将拒绝新任务,添加新任务会抛出 RejectedExecutionException 异常,此时,线程池不会立刻退出,直到线程池中的任务都执行完成。还有另一个方法 shutdownNow ,执行之后线程状态会立刻变成 STOP ,并且不再处理了还在阻塞队列中等待的任务,会返回哪些未执行的任务。

newFixedThreadPool用于创建一个“固定数量的线程池”,它的特点如下:

  • 提交任务时,如果显成熟没有达到固定数量,线程池内就会创建一个新线程,直到达到固定数量
  • 提交新任务时,如果所有的线程都在繁忙,则新任务会进入阻塞队列,该队列是无界的阻塞队列

“固定数量的线程池”适合场景: 需要任务长期执行的场景,比如处理CPU密集型任务。线程数能够比较稳定地维持在某个数量,避免频繁回收和创建线程。

newCachedThreadPool 创建“可缓存线程池”,如果线程池内某些线程成为空闲线程,“可缓存线程池”能灵活回收这些线程。它的特点大致如下:

  • 接收新任务时,如果池内所有线程繁忙,则添加新线程来处理
  • 对线程池大小没有限制,完全依赖于JVM能够创建的最大线程大小
  • 如果线程空闲(60s不执行任务),就会回收

它的适合场景:需要快速处理突发性强,耗时短的任务场景,如 Netty 的 NIO 处理场景、REST API 接口瞬时削峰场景。

newScheduledThreadPool 创建“可调度线程池”,它提供一个“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。使用方法如下:

1
2
3
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
// 参数中:0表示首次执行任务的延迟时间,500表示每次执行任务的间隔时间
scheduled.scheduleAtFixedRate(new TargetTask(), 0, 500, TimeUnit.MILLISECONDS);

“可调度线程池”的适用场景:周期性地执行任务的场景。

1.6.3 线程池的标准创建方式

大部分企业的开发规范会禁止时候用上述Java 提供的4种创建线程池的方式。,要求通过标准创建方式自行创建。标准创建的一个构造方法如下:

1
2
3
4
5
6
7
8
// 使用标准构造器构造一个普通的线程池
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收
int maximumPoolSize, // 线程数的上限
long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
BlockingQueue<Runnable> workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
corePoolSize 与 maximumPoolSize

corePoolSize 与 maximumPoolSize 自动维护线程池中的工作线程,规则如下:

  • 收到新任务,并且当前线程数少于 corePoolSize,即使其他线程处于空闲状态,也会创建新线程来处理该请求
  • 核心线程不会被回收,但是如果设置了 allowCoreThreadTimeOut 的话,则会例外,这时候是可以被回收的
  • 如果当前线程数大于 corePoolSize ,但是小于 maximumPoolSize ,那么仅当任务排队队列已满才会创建新线程。
  • 当 maximumPoolSize 被设置为无界值(如Integer.MAX_VALUE,因为这是int的最大值了)时,线程池可以接受任意数量的并发任务
  • corePoolSize 与 maximumPoolSize 的值可以动态更改

根据上述规则可知,给 corePoolSize 和 maximumPoolSize 设置相同的值可以创建一个固定大小的线程池。

BlockingQueue

BlockingQueue 也就是阻塞队列,如果线程池的核心线程都在忙,则收到的任务都缓存在阻塞队列中

keepAliveTime

用于设置线程的最大 Idle 市场,如果非 Core 线程(默认情况下只针对非Core线程,但如果allowCoreThreadTimeOut 设置为true,则也会应用于Core线程)空闲超过这个时常,就会被回收。如果要防止 Idle 线程被回收,可以将 keepAliveTime 设置为 Long.MAX_VALUE。当然,这个值也是能动态调整的。

1.6.4 向线程池提交任务的2种方式

有 submit 和 execute 两种方式,这二者的区别:

  • submit 有返回值 Future ,execute 没有
  • 由于 submit 有返回值Future ,所以可以方便对当前任务处理 Exception
  • 入参不同,submit 可以接收 Callable、Runnable 两种,execute 只能接收 Runnable
1
2
3
4
5
6
7
//submit 结果获取和异常处理
try{
Future<Integer> future = pool.submit(callable);
Interger result = future.get();
} catch(Exception e) {
e.printStack();
}

1.6.5 线程池任务调度流程

任务调度流程大致如下:

  1. 接收任务时,如果当前线程数量小于核心线程数,则创建线程(哪怕其他线程空闲),然后执行任务
  2. 如果线程池中的线程大于核心线程数量,新任务将被加入阻塞队列,直到阻塞队列满了
  3. 完成一个任务后,优先从阻塞队列中获取下一个任务,直到阻塞队列为空
  4. 当核心线程繁忙,并且阻塞队列也已经满了,如果再接收到新任务,将会为新任务创建一个(非核心)线程,并立即开始执行新任务
  5. 当核心线程繁忙,阻塞队列满的情况下,来新任务会一直创建线程,当线程总数超过 maximumPoolSize 时就会执行拒绝策略。

1.6.6 ThreadFactory (线程工厂)

1.6.7 BlockingQueue(任务阻塞队列)

阻塞队列与普通队列相比有一个重要的特点: 一个线程从一个空的阻塞队列获取元素时会被阻塞(怎么实现的?),直到队列中有了元素;当队列中有了元素,被阻塞的线程会被自动唤醒。比较常用的实现类有以下几种:

  • ArrayBlockingQueue:数组实现的有界阻塞队列,队列中元素按照 FIFO 排序。
  • LinkedBlockingQueue:基于链表实现的阻塞队列,按照 FIFO 排序,可以设置容量(有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量(即无界队列)
  • PriorityBlockingQueue: 具有优先级的无界队列
  • DelayQueue:无界阻塞延迟队列,底层基于 PriorityBlockingQueue,队列中每个元素都有过期时间,当从队列获取元素时,只有已经过期的元素才会出队,队列头部是做早过期的元素
  • SynchronousQueue :同步队列,不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入一直处于阻塞状态。

Excutors.newScheduledThreadPool所创建的线程池就是使用 DelayQueue
而,Executors.newCachedThreadPool 所创建的线程池使用的是 SynchronousQueue

1.6.8 调度器钩子方法

  • beforeExecute: 执行目标实例前在工作线程异步执行该方法
  • afterExecute: 执行目标实例后在工作线程异步执行该方法
  • terminated: 线程池终止时的钩子方法

1.6.9 线程池的拒绝策略

当出现如下情况时,新提交的任务会被拒绝:

  1. 线程池已经被关闭
  2. 所有线程繁忙,并且线程数已经达到了 maximunPoolSize

当拒绝的时候,会调用 RejectedExecutionHandler 实例的 rejectedExecution 方法,JUC 提供了以下几种实现:

  • AbortPolicy : 新任务丢掉同时抛出 RejectedExecutionException 异常,这是线程池默认策略
  • DiscardPolicy: 新任务直接丢掉,并且不会抛出异常
  • DiscardOldestPolicy: 抛弃最早的任务,然后让新任务入队
  • CallerRunsPolicy: 调用者执行策略,提交任务线程会自己执行该任务
  • 自定义策略

1.6.10 线程池的优雅关闭

优雅地关闭线程池主要涉及的方法有 3 个:

  • shutDown: 这是有序关闭线程池的方法,调用之后当前线程会立即返回,不会等待线程池关闭完成。会等待当前工作队列中剩余的任务全部执行完毕,才关闭线程池。只要调用了这个方法,线程池会转为 SHUTDOWN 状态,不会再接收任务
  • shutDownNow :立即关闭线程池,调用之后当前线程会立即返回,不会等待线程池关闭完成。
  • awaitTermination: shutDown与shutDownNow调用后会立即返回,不管线程池的关闭。而这个方法就是用于等待线程池完成关闭。

优雅关闭线程池的方法:

  1. 调用 shutDown 方法,拒绝新任务提交
  2. 调用 awaitTermination(long timeOut) 指定超时时间,判断是否已经关闭
  3. 如果 awaitTermination 超时,就可以进入循环关闭,循环一定次数(比如1000次),不断关闭线程池,直到关闭或者结束
  4. 如果 awaitTermination 返回false 或者 被中断,调用 shutDownNow 立即关闭

1.6.11 Executors 快捷创建线程池的潜在问题(系统默认提供的4种方法)

  • newFixedThreadPool创建的固定大小线程池:它的阻塞队列是 LinkedBlockingQueue 类型的无界队列,可能导致大量的任务等待,队列太大还可能导致 OOM
  • 单线程的线程池也是无界队列,可能导致OOM
  • newCachedThreadPool 创建可缓存线程池:因为核心线程数 0 ,最大线程数为 Integer.MAX_VALUE,阻塞队列为 SynchronousQueue 同步队列,不缓存任务,所以理论上任务数量不受限制,相当于来一个任务首先寻找可用线程,没有的话,就创建一个新的。会导致创建的线程过多,可能造成 OOM ,甚至把 CPU资源耗尽
  • newScheduledThreadPool 创建可调度线程池,其最大问题是线程数量不设上限,可能导致CPU资源耗尽

1.7 确定线程池的线程数

1.7.1 按照任务类型分类

  • IO 密集型: IO任务操作时间长,导致 CPU 利用率不高,常处于空闲状态。线程数确定:设置allowCoreThreadTimeOut为true、使用有界队列、corePollSize 和 maximunPoolSize 保持一致,使得接收新任务而没有空闲线程时,直接创建新线程执行,而不是在阻塞队列中等待。
  • CPU 密集型: 主要是计算任务,CPU一直在运行,利用率很高。线程数确定:CPU密集型任务虽然可以并行完成,但是并行任务越多,花在任务切换的时间也越多,效率也就越低。CPU密集型任务并行执行的数量应当等于CPU的核心数
  • 混合性任务:既要逻辑计算,又要IO。线程数确定:业界有一个公式

1.8 ThreadLocal 原理与实战

ThreadLoacal 的应用场景大致有 2 类:

  • 线程隔离
  • 跨函数传递数据

原理:每一个线程在获取本地值时,都会将 ThreadLocal 实例作为 Key 从自己拥有的 ThreadLocalMap 中获取值,别的线程无法访问自己的 ThreadLocalMap ,自己也无法访问别人的。并且,由于 ThreadLocalMap 是线程私有的,当线程销毁时 ThreadLocalMap 也随之销毁,在一定程度上减少内存的消耗。

1.8.8 ThreadLocal 综合使用案例

ThreadLocal 使用不当会造成严重的内存泄露,为了避免这种情况,使用时应遵守以下原则:

  • 尽量使用 private static final 修饰ThreadLocal ,final为了避免他人修改、变更 ThreadLocal 变量的引用,使用static 为了确保全局唯一
  • ThreadLocal 使用完后务必调用 remove 方法,这是简单有效避免引发内存泄露问题的方法

Dexposed浅析

Dexposed 对于某个函数而言,有3个hook点可供选择:函数执行前注入(before),函数执行后注入(after),替换函数执行的代码段(replace),由此衍生了很多典型应用场景:

  • AOP 编程

  • 插桩(例如测试,性能监控等)

  • 在线热更新

  • SDK hooking 以提供更好的开发体验

AndFix

腾讯系热修复方案简介

so库加载原理

Java API 提供 2 个接口加载一个 so 库:

  • System.loadLibrary(String libName): 传进去 so 库名称,位于APK压缩文件的 libs 目录,最后复制到 APK 安装目录下

  • System.load(String pathName):传进去的是so库在磁盘的完整路径,加载自定义的外部so

以上2中方式最后都调用到 nativeLoad 这个 Native 方法。

so 库实时生效方案,对于静态注册的 native 方法有一定的局限性,不能满足一般的通用性,所以放弃了 so 库实时生效需求,转而求其次,实现 so 库修复的冷部署重启生效方案。

普遍的实现方式

目前市面上很多资源热修复都参考了 Instant Run 的实现,首先看下 Instant Run 是怎么做到资源热修复的:

  1. 创建一个新的 AssetManager (AssetManager.class.getConstructor().newInstance()),并通过反射调用 addAssetPath 添加 sdcard 上的新资源包

  2. 反射所有 Activity 中 AssetManager 的引用处,全部换成刚才新建的 newAssetManager

  3. 得到 Resource 的弱引用集合,把它们的 AssetManager 成员替换成 newAssetManager

总体来说就是2步:构造一个新的 AssetManager ,并通过反射调用 addAssetPath ,这样就得到一个含有所有新资源的 AssetManager;找到所有引用到原有 AssetManager 的地方,通过反射把引用处替换成 newAssetManager 。

资源文件的格式

我们随便找个带资源的APK,用 AAPT 解析以下,可以看到内容大概是这样的:

aapt d resources App-debug.apk

spec resource 0x7f040019 com.taobao.demo:layout/activity_main:flags=0x00000000

这就表示,activity_main.xml 这个资源编号是 0x7f040019, 其中packageid 是 0x7f , 资源类型id 是 0x04 ,在Type String Pool 中对应的正是 layout 类型,而 0x04 类型的 第 0x0019 个资源就是 activity_main 这个资源。

运行时资源的解析

默认由 Android SDK 编出来的 APK 是由 AAPT 工具打包的,其资源包的 package id 就是 0x7f 。在走到 App第一行代码之前,系统就已经帮我们构建好一个已经添加了安装包资源的 AssetManager 了,即包含了 package id 为0x01 的 framework-res.jar 中资源和 package id 为 0x7f 的 App 安装包资源

如果补丁包中资源的 package id 也是 0x7f ,就会使得同一个 package id 的包被加载2次,怎么解决呢?

在Android L 之后这是没问题的,因为它会默默把后来的包添加到之前的包的同一个 PackageGroup 下面,仍旧会加入到该类型的 TypeList 中,只是会打出一个 warning log。但是,使用时获取某个 Type 资源时,会从前往后遍历,也就是说先得到原有安装包里的资源,补丁包中的资源永远无法生效了。所以,在Android L 以上的版本,在原有的 AssetManager 上加入补丁包,是没有任何作用的。

而在 Android 4.4 以下版本,addAssetPath 只是把补丁包的路径添加到了 mAssetPath 中,这时候早已经错过真正解析资源包时间了。

以上解释了为什么像 Instant Run 这种方案,一定需要一个全新的 AssetManager ,然后再加入完整的新资源包,替换原有的 AssetManager。

另辟蹊径的资源修复方案

一个好的资源修复方案,首先补丁包要足够小,直接下发完整的补丁包肯定是不行的。目前主要有以下方案:

  • 对资源包做差量处理,在运行时合成完整包,虽然减少了包体积,但是运行时的合成操作耗费了运行事件和内存。

  • 自己修改 AAPT ,在打补丁包时对资源重新编号,这样会涉及修改 Android SDK 工具包,不利于集成,也无法很好地对将来的 AAPT 升级。

我们的方案:构造一个package id 为 id 0x66 的资源包,它只包含改变了的资源项,直接在原有 AssetManager 中 addAssetPath 这个包即可。补丁包的资源,只包含原有包没有而新报里面有的新增资源,以及发生了改变的资源。对于增加、减少、修改这3种情况,我们要如何处理呢?

  • 新增资源直接假如补丁包

  • 减少资源,只要不使用这个资源就好了

  • 修改资源,比如替换了一张图片,那么将其视为新增资源,在打入补丁包的时候,代码引用处也做相应修改,把原来使用的旧资源id的地方变为新的 id

一张图说明下这些情况(绿线表示新增资源,红线表示发生修改的资源,黑线表示内容没有变化,但是id发生了变化的,x表示删除了的资源):

资源补丁包构建

新增的资源及其导致的id偏移

新资源插入的位置是随机的,这与每次 AAPT 打包时解析 XML 的顺序有关。

所以,新增的资源导致它们所属的 type 中跟在它们之后的资源 id 发生了位移,发生位移的资源不会加入补丁包中,但是在补丁包的代码中会调整 id 的引用处,如下所示:

1
imageView.setImageResource(R.drawable.holo)

R.drawable.holo 是一个int 值,它的值是 AAPT 指定的,可以用反编译工具可以看到它的真实值:

1
imageView.setImageResource(0x7f020002)

打出一个新包时,对于开发者而言,holo 的图片内容没变,代码引用处也没变,但是在新包里面,同样这句话,由于新资源插入导致 id 改变,所以引用实际变成了:

1
imageView.setImageResource(0x7f020003)

但这种情况不属于资源改变,更不属于代码改变,所以,我们在对比新旧代码之前,会把新包里面的这行代码修正为原来的id:

1
imageView.setImageResource(0x7f020002)

内容发生改变的资源

内容发生改变,比如 activity_main.xml 文件内容改变了,也可能我们修改了string 类型的值,他们都会加入到补丁包中,并重新编号,相应代码也会改变:

1
setContentView(R.layout.activity_main)

实际上就是:

1
setContentView(0x7f030000)

在生成新旧代码对比之前,我们会把新包里面的这行代码改为:

1
setContentView(0x66020000)

这样,新旧代码对比时,会检测到这行代码发生了改变,于是相应的代码修复会在运行时发生,这样就得到了正确的新内容资源。

删除了的资源

不影响补丁包,就不多言

对于type的影响

上面说的所谓简单,值得是运行时应用补丁变得简单了。真正复杂的地方在于构造补丁。

更优雅地替换 AssetManager

分2种情况:

  • 对于 Android L 以后的版本,直接在原有 AssetManager 上应用补丁就行了,并且由于是应用原来的 AssetManager ,与 Instant Run 方案比,省略了大量的反射和替换操作,提升了加载补丁的效率

  • 之前提过,在Android KK 和以下版本,addAssetPath 是不会加载资源的。我们对原有的 AssetManager 先进行析构,再重构的时候将补丁包资源也加入,用的还是原来的 AssetManager,同样避免了反射和替换操作(Instant Run 方案还是必须重新构造一个新的 AssetManager 并加入补丁包,再替换原来的)

意料之外的资源问题

在加载完补丁之后,如果做了 new WebView() 操作,就会发现找不到新资源的问题,这是因为 WebView 初始化时可能会构造新的 ResourceImpl ,替换掉了原先的 ResourceImpl ,从而把补丁资源给一起丢掉了。其他的就不展开。

小结

对比市面上的方案,我们的方案优势:

  • 不侵入打包,直接对比新旧资源即可产生补丁资源(对比修改 AAPT)

  • 不必下发完整包,补丁中只有变动的资源(对比 Instant Run、Amigo等方式)

  • 不需要在运行时合成完整包,不占用运行时的计算和内存资源(对比Tinker)

唯一需要注意的是,因为对新资源的引用是在新代码中,所有资源修复需要代码修复的支持的

可能的问题:查找旧 id 的时候,是直接对 int 值替换,所以会找到 0x7f?????? ,如果开发者也使用了 0x7f?????? 这样的数字,就会导致数字会被错误地转换。

1、内存泄漏怎么检测?LeakCanary 原理

2、HashMap 源码?为什么要将链表转成红黑树?红黑树特点?

3、消息循环源码、如何做到任务切换线程?想要提交一个任务有几种方式?

4、多个Fragment在销毁后重建之后重叠怎么办

点击看答案

产生重叠的原因

一般是由于我们采用show 和 hide 的方式( 而不是replace )来控制Fragment 的显示和隐藏,当Activity 由于内存不足被回收之后再恢复的时候,如果不做处理,那就会把这些fragment new 出来添加到Activity 中,由于是刚new出来的,并没有以前的show/hide 状态,因此就造成重叠。

解决办法

在第一次进入Activity 初始化Fragment的时候,为添加的fragment 设置tag ,并且 tag 为fragment的全限定名,将这些tag以列表形式tags保存。在 onSaveInstanceState 回调的时候,保存tags。

在恢复的时候,首先恢复出来tags,再依次遍历tags中的tag,使用FragmentManager.findFragmentByTag 查找fragment。如果查找出来的 fragment 不空,则直接使用,否则的话,就使用 savedInstanceState?.classLoader.loadClass(tag) 来反射新建这个fragment (因为我们的tag就是fragment 的全限定名嘛)。

replace 方式切换fragment 与 show/hide 方式切换fragment

replace 固然可以避免很多不必要的问题,但是它不能保存页面状态,对用户体验不友好,频繁切换还会导致卡顿。show/hide 方式只会控制隐藏/展示,所以效率上会好一些。

以上内容参考自简书上的博客披萨大叔

5、设计一个图片缓存框架,缓存算法用什么

点击看答案

缓存算法使用 LRUCache,其原理可以参考Android基础-LruCache原理

6、有个ListView快速滑动 如何优化Bitmap的显示

点击看答案

监听列表的 onScrollChangeListener ,在快速滑动的时候, Fresco.getImagePipeline().pause() ,停下之后又 resume() 操作。

以上内容可以参考:cnblogs上的博客

7、Bitmap的复用听过没有

8、kotlin 协程原理?怎么切换线程的?Kotlin 优点和缺点?

9、调优、怎么瘦身,打包流程,模块化

10、开发者模式中,GPUInfo 中柱状图颜色代表的含义;命令dumpsys meminfo中VSS、PSS、GSS、USS代表什么意思

11、Android绘制三部曲、Canvas是怎么最终显示在屏幕中的、是以什么样子的数据结构传输的

12、物理内存与虚拟内存的关系、Android Heap的结构是怎么样的、如果要进行垃圾回收,会收集那些区域

13、binder源码、其原理是什么,发起一次请求的过程,如何根据文件描述符找到对应的binder实体

14、如何进行内存优化 减少内存消耗

1、try catch finally关于return的执行结果

2、treemap如何对两个元素进行比较? 非compareble对象比较?

点击看答案

如果是非 compareble 对象,则通过构造函数传入自定义的 Comparetor 比较器。

参考java基础第10个知识点

3、object有什么方法

点击看答案

clone、equals、finalize、getClass、hashCode、identityHashCode、identityHashCodeNative、internalClone、notify、notifyAll、toString、wait

以上内容参考自系统源码

4、讲讲Android存在的设计模式

点击看答案
  • Context 的装饰器模式
  • AlertDialog 使用了建造者模式
  • BitmapFactory 的工厂模式
  • View 的layout、measure、draw 就是 责任链模式
  • aidl 代理模式
  • ListView、Gridview 等 使用的适配器模式

以上内容参考自简书上的文章

5、android的事件分发

6、android的消息机制

7、LeakCanary

8、讲个android的源码 自选

9、hashcode 与 equals理解

10、设计模式讲讲有什么认识的,代理模式优缺点

点击看答案

代理模式优点:

  • 协调调用者和被调用者,降低耦合度
  • 增强被调用者的功能。增加和更换代理无需修改被调用者源码,如何开闭原则,具有良好的可扩展性。
  • 保护目标对象

缺点:

  • 增加了类
  • 速度可能慢。在调用者和被调用者之间增加了代理对象,因此请求处理的速度可能会变慢
  • 增加系统复杂度。实现代理模式需要额外的工作,有些可能还比较复杂,比如远程代理。

以上内容参考自简书上的博客

10、图片 缓存机制

11、首屏调优 内存调优

lazada

1、自我介绍一下

2、app网络防劫持 介绍下,出现劫持,解决方案呢?拦截了webview 网络请求后,用什么请求

3、性能优化,做了哪些工作

4、强引用、弱引用和软引用。弱引用和软引用的区别。

5、MVC 和 MVP 的区别。他们各个层次之间交互有什么区别

6、SharedPreference 是线程/进程 安全的吗?apply 和 commit 有什么区别?(提交到哪?是本地的xml 还是 内存的xml?),有什么坑?

7、Handler 机制说一下? 消息延迟是怎么做到的?我有个新消息,在已有的消息队列中我想让他先执行,可以吗。

8、最有挑战的项目?你负责做了什么?给公司带来什么价值?,这个sdk给你们的产品带来什么价值?如何判断你这个sdk是否好用?你们的业务成功率是多少,有统计吗?如果要设计这个监控这个sdk?你们有相关的埋点吗?感觉面试官隐约对app的监控感兴趣。

点击看答案
  1. 被动式,大致就是 APM 监控启动时间、ANR、卡顿、崩溃等,这几种监控手段在灰度以及线上都发挥作用,性能上进行监控主要依赖自动化,自动化测试能发现超过一半的性能问题,业务上进行监控便主要是通过埋点进行统计,加上业务失败率告警,以上都要对各种失败率有一个明确的界定 比如崩溃率不超过万三这样

  2. 主动式,提供反馈渠道,对用户的反馈有一套完整的流程体系,对业务进行分发,反馈能及时与用户沟通,用户也能看到反馈的状态

  3. 防劣化手段, 比如关键api hook 统计耗时, 针对性使用框架 (leakcanary这些)

9、http 和 http2 有什么区别。

10、http 和 https 有什么区别。告诉我它是怎么做到安全的呢?

11、kotlin 语言与java 语言相比有什么优点吗?

12、kotlin 的协程了解吗?它的原理?协程相比线程创建的开销小一些?主线程使用协程是不是不会阻塞?假设我在主线程里面使用协程执行非常耗时的操作,我的主线程会ANR吗?为什么在io操作比较多的时候对协程有利呢?协程有自己的栈空间吗?

BD

1、线程同步方法(关键字,并发包)

2、wait/notify,notify方法和notifyAll方法区别,notifyAll方法唤醒的线程,怎么决定哪一个线程得到运行

3、HashMap(要很熟悉),put过程,get过程

4、现在key为double类型,需要将key的整数部分当成同一个key应该怎么做(提示:重写方法)

5、内存泄露分析

6、进程间通信方式(别忘了BroadcastReceiver)

7、A跳转到B的生命周期调用,如果A的onStop方法方法发生在B的onResume方法之前会发生什么现象,如果B是singleTask又是什么情况?会调用B的哪些方法

8、service运行在什么线程

9、Handler内存泄露原理(message.target = handler,所以真正原因是由于message造成的),解决方法,一个线程可以有多个Handler吗。

10、handler的sendMessage方法和postRunable方法区别

11、HandlerThread

12、设计一个图片加载框架,LRUCache原理

13、看过第三方库的源码吗

14、排序算法,时间复杂度,快排为什么是NlogN

15、完全二叉树高度

16、弱引用和软引用

17、今天面到的题目新的部分有几个 1include merge viewstub区别 2二叉树第n层节点个数 3touchdelegate

BD

1、http状态码

2、onTouch、onTouchEvent、onClick 执行顺序

3、onclick 不响应,如何排查

4、设计一个下载框架(考虑多线程)。

5、单进程中,一个线程下载和用两个线程下载哪个更快?

点击看答案

决定用户下载大文件速度快慢的终极因素,在于用户下载进程实时抢占网络带宽的大小。其它的因素与它相比,可以忽略不计。任意一个与互联网通信的进程,理论上都有一个实时最大可用带宽,这是客观存在,不以用户意志为转移。 如果 用户进程实时抢占的带宽 = 实时网络可用带宽 那是最最理想的,用户进程100%利用网络带宽,无论进程(Process)是单线程(Thread)的还是多线程的,下载速度几乎没有任何区别。

参考: 为什么下载大文件时多线程比单线程速度更快?

6、在主线程要优先执行一个任务咋办?除了消息栅栏外还有其他方法吗?Thread.join,还有吗?如何发送消息栅栏,这是hide方法,自己如何发送异步消息。

点击看答案

可以参考 提高Handler发送消息的优先级
postAtFrontOfQueue、sendMessageAtFrontOfQueue

7、空闲的时候执行某个任务有什么方法?IdleHandler,还有其他方式吗

8、IntentService 与 Service 区别,啥时候用IntentService

9、synchronized 锁优化策略

10、线程安全的集合有哪些?CopyOnWriteList 在什么时候使用?HashTable 与 ConcurrentHashMap 的区别?

11、线程池的拒绝策略有哪些?

12、什么情况下singleTask 在一个进程中会有两个实例? NEW_TASK

13、A启动B,B启动C,C启动D,现在从D启动A,并且启动完成后只保留A,如何做?

14、Okhttp 的 addInteceptor 与 addNetWorkInterceptor 的区别

15、如何保证dns的安全?自己建立dns服务器,httpdns

16、做sdk的话,如何在接入的时候管理第三方依赖

17、设计模式,整体和部分

18、Okhttp 多个host,如何配置

TX

a启动b,b启动c,c再启动d,这时候直接回到a页面,并且只保留a页面,有啥方法?

okhttp多个host,多重配置

异步创建view的原理

如何插桩

哪些地方匿名共享内存:堆,contentprovider,显示的时候

sp原理

事件分发

hashtable concurrentHashMap

TX

1、了解插件化吗?有哪些第三方开源的方案?Tinker的原理

2、行业内性能优化的方案有了解吗?页面如何秒开?

3、串行GC的时候,会锁住堆挂起线程,那么无论线程处于什么状态都能挂起吗?

4、多线程中的同步队列与等待队列(可能问的是 同步队列和条件队列)

5、CAS是啥?AQS是啥可以参考这里?RetraintLock的原理

6、锁的种类,什么是可重入锁

7、如何让多个线程同时启动

8、Java内存空间怎么划分

9、有哪些GC算法,哪些对象可以作为GC Root

10、JNI 中,Env是啥,它能干什么

11、什么情况下本地方法栈会引用Java对象

12、HashMap原理

13、Android系统的启动流程,应用为什么用zygote来fork,有什么好处

14、Apk的签名原理,v1和v2

15、https相比http的优势是什么?除了安全之外呢?证书如何校验

16、Java的堆与栈的区别,除了存储的对象不一样之外,还有其他不一样么?

17、进程间通信为什么用Binder,Binder的原理?空间大小限制(同步调用是1M - 8k ,异步oneway 就要(1M - 8k)/2 )?

18、可重入锁是什么?悲观锁和乐观锁的区别?

19、什么时候用到TCP,什么时候用到UDP?举例

20、https的攻击手段,https的证书是如何发送给客户端的?

21、RecyclerView和ListView的区别

BD

handler,IdleHandler原理,使用

组件化,插件化,热修复

断点续传

用过哪些开源库?retrofit的原理

反转二叉树

做了哪些优化?gpu优化?

hashmap原理,如何hash?为什么8个的时候要红黑树?

a启动b生命周期怎么走

gc算法,哪些可以作为gc root,分代算法是怎样的?

volatile关键字作用?能保证原子性吗?

webview内存泄露的原理,handler内存泄露原理

如何发现内存泄露?LeakCanary的原理是什么?

Activity的启动模式

taskaffinity的作用?

进程间通信方式有哪些?AIDL能传递的数据类型有哪些?AIDL怎么使用?AIDL的方法参数能传接口吗?都跨进程干什么

app崩溃率多少?怎么统计的?能捕捉所有异常吗?如何捕捉native的崩溃?

动态代理

TX

relativelayout与constrantlayout(可以参考链接:https://mp.weixin.qq.com/s/gGR2itbY7hh9fo61SxaMQQ)

如何查看cursor之类的导致内存泄露问题

onclick,onTouch执行顺序

算法:链表的中间元素,判断链表是否有环,快排最好最差时间复杂度,快排是怎么操作的

TX3 面试

近期有做什么优化或者重大技术?
DNS污染是怎么回事
https的证书是怎么起作用的?
用什么检测内存泄漏-LeakCanary
扫码登录是怎么做到的
多线程用过吗,乐观锁与悲观锁说下

TX1

String可以继承吗?为什么?为什么设计成不可变?
快排原理
二分查找原理
Activity的几种启动方式,Service中怎么启动Activity(参考链接:https://blog.csdn.net/fang323619/article/details/74388804),从源码角度来说,ActivityManager 为什么会对4种tag做处理

事件传递的机制(触摸事件)?Activity 的顶级响应视图是啥?onTouchListener 与onClickListener 哪个先执行(onTouchListener)?onTouchListener 返回true 之后,onClickListener 还会执行吗(不会)?onTouchListener 和 onLongClick 哪个先执行?

View的绘制流程?从掉帧的角度来说下 View 的绘制流程?
ScrollView 嵌套滑动冲突怎么去解决?在父view的up里面return 了true,子view会收到down事件吗?
tcp与udp区别,tcp三次握手做了什么
单例模式怎么写,为什么
大图一般是怎么优化的?fresco的源码有看过吗,它有什么缺点?有没有用过其他的图片框架?

点击看答案 1. 将适合 UI 组件展示的图片尺寸加载到内存中,而不是使用原图。 2. 将BitmapFactory.Options 设置 inJustDecodeBounds 属性设置为 true,可以在解码时避免内存的分配,从而避免将原图加载到内存,而是将缩略图直接加载到内存。 3. 在 Android 中,我们常见的一些颜色设置,都是 RGB 色彩模式来描述像素颜色的,并且他们都带有透明度通道,也就是所谓的 ARGB。 4. Android 中的图片在加载时,默认的色彩格式是 ARGB_8888,也就是每个像素占用 4 个字节空间。我们需要针对不同的应用场景做不同的处理,大图和小图可以采用不同的解码率(也就是设置不同的色彩格式)以优化图片内存的使用。 5. 当存应用中存在大量图片时,使用缓存,可以有效的提高内存的使用率以及用户体验。 6. LruCache 类特别适合用来缓存 Bitmap,它使用一个强引用的 LinkedHashMap 保存最近引用的对象,并且在缓存超出设定大小时,删除最近最少使用的对象。 7. 磁盘缓存可以用来保存那些已经处理过的 Bitmap,它还可以减少那些不再内存缓存中的 Bitmap 的加载次数。当然从磁盘读取图片会比从内存要慢,而且由于磁盘读取操作时间是不可预期的,读取操作需要在后台线程中处理。 8. 内存缓存的检查是可以在 UI 线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在 UI 线程中发生。当图片处理完成后,Bitmap 需要添加到内存缓存与磁盘缓存中,方便之后的使用。 9. Fragment 具有属性 retainInstance,默认值为 false,当设备旋转时,fragment 会随托管 activity 一起销毁并重建。调用 setRetainInstance(true) 方法可保留 fragment,已保留的 fragment 不会随着 activity 一起被销毁,它会一直保留(进程不消亡的前提下),并在需要时原封不动地传递给新的 Activity。另外,Fragment 中定义了isAdded 接口,用于判断 Fragment 是否已经绑定到某个 Activity。 10. Android 图片使用的对象最终都是用 Bitmap 来创建并存储的。在不同的 Android 版本中,Bitmap 或多或少都存在差异,尤其是在其内存分配上,针对于不同的 Android 版本,Bitmap 的推荐的内存管理策略也会存在一些差异。 11. 在Android 2.3.3 (API level 10)以及之前, 一个 Bitmap 的像素数据是存放在 Native 内存空间中的。这些数据与 Bitmap 对象本身是隔离的,Bitmap 本身被存放在 Dalvik 堆中。并且无法预测在 Native 内存中的像素级数据何时会被释放,这意味着程序容易超过它的内存限制并且崩溃。 12. 自 Android 3.0 (API Level 11)开始,像素数据则是与 Bitmap 本身一起存放在 Dalvik 堆中。 13. Android 8.0(Android O)及之后的版本中,Bitmap 的像素数据的内存分配又回到了 Native 层,它是在 Native 堆空间进行分配的。 14. 尽量减少 PNG 图片的大小是 Android 里面很重要的一条规范。相比起 JPEG,PNG 能够提供更加清晰无损的图片,但是 PNG 格式的图片会更大,占用更多的磁盘空间。推荐使用 Webp 格式的图片。 15. 我们可以利用 inBitmap 这个属性来提升 bitmap 的循环效率。使用 inBitmap 属性可以告知 Bitmap 解码器去尝试使用已经存在的内存区域,新解码的 bitmap 会尝试去使用之前那张 bitmap 在 heap 中所占据的像素数据内存区域,而不是去问内存重新申请一块区域来存放 bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。 16. 使用压缩工具对图片进行二次压缩。 17. 根据最终图片是否需要透明度展示,优先选择不透明的图片格式,例如,我们应该避免使用 png 格式的图片。 18. 对于色彩简单,例如,一些背景之类的图片,可以选择使用布局文件来定义(矢量图),这样就会非常节省内存了。 19. 大图: BitmapRegionDecoder 展示部分

参考: CSDN的博客

Activitymanagerservice 源码
知道哪些app性能优化

lazada

自我介绍
性能优化做了哪些工作
四种引用的含义
mvp 与 mvc 区别
Handler 机制,如何实现消息延迟
最有挑战的项目,负责做了什么
业务监控怎么设计
http与http2的区别
http与https的区别
kotlin语言与java的区别,优点
kotlin的协程原理,有自己的栈空间吗,与线程区别

英语自我介绍下?–妥妥挂了。。

猫总

为什么不用URLConnection而用OKHttp不了解

qq的music

自我介绍下
性能优化的经验
如何设计措施,防止以后新人在主线程添加太多的任务
了解过启动框架吗
如何分阶段启动相关任务,这些任务有相互依赖,或者有向无关图
能达到什么效果,量化下
内存优化的内容
webp 占用内存小一些还是尺寸小一些?
图片框架本来就会做这些裁剪之类的功能,这样自己做是否就重复了
其它内存优化的点(我觉得可以说说大图加载,只加载一部分)
了解ashmem 吗,除了这个作用还能有其他作用吗
优化app的整体内存占用
第一步得分析吧。profile 工具
第二步、分析问题在哪,native 和 Java 层 的内存占用,如果native 和 Java 中图片占用都比较大,如何分析
第三步、是内存泄漏导致的、还是大数组、大对象

用LRU 的话,一般不会引起内存很大占用,所以如果内存大,一般是内存泄漏导致的

Talkingdata 统计些什么

监控dns的成功率,即用户使用你的ip直接请求的成功率了,以前系统直接解析,现在是用户直接打不开

尝试使用https 去解决吗

网络成功率怎么优化

dark 和 art 的 gc 过程有了解吗

非并行 GC 的过程

分配内存,不够,然后做什么操作,最后就会分配成功。分配不够的话,系统会扩容,扩容发生在什么时机

常用设计模式,单例模式怎么写

volatile :

性能优化了解不太多,尤其内存优化总结不多,数据量化

BD

Fresco 架构上说下,MVC模式,说说 Fresco加载gift的流程和原理
4. 一个大的ViewGroup,上面显示两个不一样大小的图片,内容一样,怎么做(利用Fresco的缓存)
5. 一个图片是50 * 50,一个图片是100 * 100,内存大小是怎样的,比如 5050的内存大小是1,100100的内存大小是多少? 两张图片,内存变大了还是不变,内存副本有几份?
6. RecyclerView 滑动的时候,缓存机制
7. RecyclerView 下面一个子item,我手指按在RecylerView上,然后滑动,说出滑动过程
8. 海量数据中找出TOP N(堆)
9. 自定义帧动画机制,一边解压一边加载,用完之后回收,inBitmap进行内存复用。用了哪些数据结构,如何保证按顺序执行。
10. bitmap drawable 区别(互相转化,drawable包含bitmap)Bitmap是Drawable . Drawable不一定是Bitmap Drawable在内存占用和绘制速度这两个非常关键的点上胜过Bitmap

1、 jni 方法调用是怎么实现之类的
2、 java string 和 c++ string 有什么区之类的
3、 自己做的项目上遇到的技术难题和解决方案
4、 消息队列

看点1

React Native了解它的实现原理吗?除了组件映射,还有更深的理解吗?
Android哪一块比较熟比较擅长,聊聊?
可以给子线程创建消息队列吗?看过消息队列的底层原理吗?对,Native层的代码
Android里面什么是ANR?遇到ANR怎么排查?
硬件加速和非硬件加速有什么区别?
HARDWARE_LAYER 与 SOFT_LAYER 分别代表什么意思?
UI性能相关性能的优化有没有做过?比如一个列表滑动很卡顿,怎么优化?如何查看占用内存和cpu?GC怎么会影响性能?有什么手段可以降低布局层级?
Recyclerview与Listview的缓存原理?不同类型的item他们怎样缓存?
Android内存怎么优化?什么是内存泄漏?什么情况会出现内存泄漏?实际中举个例子?Handler为什么会泄漏Activity?
图片展示比较多的App,怎么解决内存占用问题?用了什么图片框架?为什么Fresco 在5.0以下具有优势?Fresco的实现原理,大概流程是怎样的?

内存中未解码图片是啥样?
怎样为图片框架设置内存缓存策略?怎么决定淘汰哪些内存?
还有没有了解其他图片框架吗?
怎样尽量降低位图占用的空间大小?
图片列表,怎么优化性能?
视频播放相关的经验有吗?播放器用的是什么?videoView用的是哪个?TextureView 和 SurfaceView 有什么区别?哪个性能更好?为什么?这两个view和Android中普通的view有什么区别?使用方式有什么区别?

SurfaceTexture,TextureView区别

我们知道,一般Activity 包含多个View,他们组成树状结构,但是只有顶层的 DecorView 才是对 WMS 可见的,它在 WMS 中有一个对应的 WindowState ,相应地,在 SF中对应 Layer。

而SurfaceView 继承自 View,因此本质上是个View,但是它有自己的Surface,这个 Surface 在 WMS 中有对应自己的 WindowState ,在 SF 中也会有自己的 Layer。如下图所示:

SurfaceView示意

这样,虽然SurfaceView 仍然在 View 目录树中,但是其实它与主窗口是分离的,这个的好处就是这个surface的渲染可以放到单独的线程去做,渲染时,而已有自己的GL context,这对于游戏、视频等性能相关的应用非常有益,因为它不会影响主线程的响应。但是同样也有缺点:因为它有自己的Surface ,所以它不受view的属性控制,因此不能进行平移、缩放等变换以及动画

而 TextureView 在View 也继承自View,在目录树中以普通的View存在,因此支持旋转、缩放 以及动画,但是 TextureView 必须只能在支持硬件加速 的窗口中,它对draw方法进行了重载,把收到的数据作为纹理更新到对应的HardwareLayer 中

看不大懂,参考资料CSDN

你觉得视频播放怎么优化视频首帧加载速度?有看过Exoplayer的实现吗?它的解码过程和原理?
FFmpeg有了解过吗?
有做过C/C++之类的开发吗?大概是个多大的代码量?
有用过OpenGL吗?
对Android的组件化有了解多少?了解Android的热修复吗?热修复的原理是什么?
https怎么实现安全的?https的握手比http的握手多了几个RTT ?
http 1.0 、2.0、3.0 之间的区别?http3.0 最大的优势?
其他看过的第三方开源的框架?

TX MUSIC

单例怎么写呢?volatile关键字在这里面的作用。如果我不需要懒加载,什么时候不需要做懒加载?

关于单例的饿汉和懒汉加载模式,目前知道的一个答案就是:类中还有其他静态方法或变量时,饿汉模式才有优势,此时可以通过访问任一静态成员来实例化对象,这缘于首次访问静态成员会引发类的加载,而类加载时会初始化静态变量,具体可以参考别人的提问

用过反射吗?App发布的时候,需要混淆的,混淆对反射有什么影响?有没有用过Gson,有什么功能?知道它的工作原理吗?怎么知道类的成员变量的名字?我是一个泛型,我是一个List,类型是String,反射能不能拿到泛型的数据?泛型你知道有上界和下界这个东西?他们有什么用?

sharedpreference 的apply 和commit有什么区别?apply什么时候同步到磁盘上去(pause和广播,跟广播有啥关系?)?apply是异步的对吧?

有接触过插件化?插件化的原理是什么?怎么欺骗系统。

handler的工作原理?如何移除消息屏障?handler怎么导致的内存泄漏?

防止二次打包,是怎么做的?现在拿到一个包,想二次打包,而如果他做了二次打包的验证,你怎么绕过?我拿了别人的库,别人会校验签名,如果我想用这个库,怎么弄?你们的签名验证怎么做的?

kotlin印象深刻的特性,举例一下?异步的代码写成同步,怎么写?怎么做到异步能写成同步的?正常来讲,我们是从上往下执行的,为毛它可以在中途切线程?怎么做到让出线程使用权限呢?但是如果是使用Java的wait、notify会阻塞,但是kotlin不会啊,咋做的?用kotlin从子线程切换到主线程,怎么做?GlobalScop 有什么不好的点吗?kotlin的空安全了解么?在定义变量的时候,就指定是否可以为空,你觉得这样好不好?lateinit的作用是啥?什么情况比较适合使用 lateinit?

如果 lateinit 和 lazy 对比,lateinit 只用于变量var,而 layzy 只用于常量val,一般用于单例在变量第一次调用时才执行。其次,基于我个人的理解,lateinit 适用的场景是你不想指定一个变量为可空类型,但是又得在稍后(或者得等到满足某个参数时,假如RecyclerView的Adapter对象,可能得等到某个callback 初始化之后,才能作为参数来new 这个Adapter)才能初始化的那种变量。以上参考简书的博客

https怎么做到安全?https的流程?怎么校验证书?你是说自己也计算一遍对吧?服务端发证书给你,中途被我换了,客户端能不能发现被换了?用我自己的私钥签名了,客户端如何发现的?证书链怎么验证?我做了个代理,做了个中间人攻击,一般我能听别人https的内容,为什么我能做到呢?你们本地https的证书校验的证书从哪里来?那你获取证书这部分我是不是可以中间人攻击,你们发的是tcp包还是http包?http包那你们也还是会有明文,就会有漏洞。对称加密的密钥是怎么生成的?为什么需要客户端和服务端一起作用来生成?为什么后面需要使用对称加密来做?

知道DNS会有劫持吗?你们的情况不一定是dns劫持,可能是别人直接把你们的Http包给替换了?httpDNS 有什么好处?-更科学地翻译成ip,深圳访问不会被分配到北京。

数组,找到数组里面第一个重复的数字,怎么做?HashSet去做时间复杂度多少?意思是遍历一次就行了是吧?

如果App出现了频繁的GC,怎么排查?怎么排查有没有内存泄漏的?-leakcanary ,如果在IDE里面使用工具呢?如果我想看哪些对象被频繁申请,怎么搞?

看起来面试官想要说出MAT,可以参考下别人的博客。但是其实目前已经无需MAT就能直接在AS里面操作了

BD

挑一个比较有价值的项目聊聊?500彩票,那聊一聊做了哪些事情?模块化和mvp讲讲,mvp有什么优缺点?-MVP类增加了,寻找逻辑没那么直接。有没有做成组件化的形式吗,打包成aar之类的发布?

插件化讲一讲?打包怎么办?插件用到宿主的资源咋办?怎么识别类,是将dex添加到dexlist中还是自定义 ClassLoader ?启动service、广播、ContentProvider?Activity去hook它的mCallback,但是假如高版本的时候不让你去反射咋办?宿主里面的类更新了,插件版本没有更新,这情况咋办?

讲一下handler吧?

用过哪些第三方组件,开源库?网络库用的啥?用过github上哪些轮子啊?okhttp源码有看过吗?他有很多拦截器,他是怎么弄的输入输出?

注解有用过吗?除了用来替代枚举外还在哪里用过吗?

写个算法题:单向链表,自己去定义结点,他有个值,还有个next指向下一个。我们输入两个这种链表,找到他们第一个公共节点返回,没有的话,返回null 。

BD二

做了哪些非业务的工作?所理解的网络劫持是什么样的?怎么发现网络劫持和预防?https可以解决DNS劫持问题吗?如果要篡改http可以在哪些环节篡改?你觉得谁会来篡改你们的App网络报文?https为什么可以保证不被篡改?介绍下Https的流程?我们要分析https的请求的话,可以怎么做吗?App又怎么去防这种事情呢?服务端拉取证书怎么保证安全呢?-ip直连去拉取证书。量很大的情况下,都通过ip去获取证书的话,服务端会不会扛不住?如果ip挂了,或者运营商给你封了,那你怎么办?-内置证书,防止重打包 。做了一些安全方面的事情,是哪些?为什么App签名本地校验是防君子不能防小人?

sp做加密,出于什么目的?别人怎么读到你的sp存储的内容?哪些存储的内容需要加密?sp写的时候加密以及读的时候需要解密,这个性能会有影响吗?业界有没有好的方案来解决Sp吗?-替代方案是MMKV 。MMKV 是如何解决跨进程问题?为什么SP跨进程是不好的?

性能上做了一些优化,做了哪些事情?App启动优化做了哪些事情?内存优化主要做了什么?hashmap 换成arrayMap 等会有多大的收益?这个内存收益怎么衡量?-启动App进入到彩票购彩页面。防止内存泄漏怎么去做的?线上如果要做一些这样的监控怎么做?-talkingdata 。 它的原理有去了解吗?内存泄漏经常会OOM,这种OOM怎么在线上怎么监控?-发生的时候,dump内存信息上传分析。更详细的没有去了解是吧?

算法题目:给定一个字符串S,返回 反转后的 字符串。反转的规则:其中不是字母的字符都保留在原地,而所有字母的位置发生反转。

例如: 输入:”ab-cd” 输出:”dc-ba”
输入:”a-bC-dEf-ghIj” 输出:”j-Ih-gfE-dCba”

BD

视频播放采用什么播放器?视频播放首帧优化?有其他方式来优化视频播放,比如卡顿等体验?视频播放碰到过什么问题?

选择SurfaceView还是TextureView ?视频有做过加密吗?视频地址是固定的吗?

视频格式了解吗?分辨率、帧率、码率 都是什么含义?

git 的merge 和 rebase 有什么区别?代码从本地到远端是怎么流转的,比如我新建一个文件,怎么到远端的?

性能优化做了哪些内容?

json解析耗时几百毫秒,是怎么统计到的?systrace 用过吗?

不重要的任务是怎么去初始化的?

用过线程池吗?对线程的管控怎么搞?如果有人在代码中直接去new Thread,这个怎么管控?

RxJava里面线程池的线程有限制吗?RxJava的原理有多少?RxJava的线程切换怎么实现的?RxJava是怎么切换的线程,比如他observeOn 和 subScribeOn 这两个操作符?这两个操作符有什么区别?多次调用他们会多次生效吗?为什么不能多次生效?

协程的原理?同样作为操作线程的工具,它和RxJava的本质区别是什么?与RxJava比较还有没有别的优势?kotlin的作用域函数apply、also等,怎么确定什么时候用什么?

图片规范的方式去使用,怎么规范?

fresco 有几级缓存?

组件化怎么弄的?组件化做到什么程度?

module之间怎么通信?除了基础module,其他的module A 想要和 module B 通信,A启动B里面的一个页面?

为什么选择MVP?P层可能会无线膨胀,有没有什么解决办法

最近jetpack的组件有没有了解?

如何设计一个组件化的app
View的绘制流程
activity的启动模式
如何监控启动就crash的问题
如何独立编译单独的组件
对比一下图片加载模块,Glide 和Fresco , AsyncImageView

主要还是通过项目发散开的问题:
1,android开发中常见的两种序列化,serializeble 和 parcelable 的区别
2,recyleview中的常见卡顿问题是如何解决的,如何监控到用户的卡顿(监控UI线程的message 处理逻辑,超过60ms)
3,项目比较大,编译比较慢的解决办法(插件化,组件化)
4,项目中遇到的一个小问题,是如何解决的
5,OOM类型的问题如何解决
6,内存泄漏如何监

Android so如何减包
Android Camera采集渲染流程
如何排序一个比内存大的文件

C++ 父类析构函数的virtual作用,怎么实现防止内存泄漏的
C++ 虚函数表是什么
JNI的attachCurrentThread作用是什么
JNI的LocalRef和GlobalRef new了是否要delete,可以不delete么?
简单介绍下libuv

BD3

http与https的差异有多少?https这个安全层是怎么做的,怎么保证安全的?非对称加密是在哪里使用?数字证书除了公钥之外还有其他的吗?数字证书和数字签名有什么关系?验证双方身份的时候,用了个什么结构,比如说,我发了个信息给你,怎么验证是我发给你的?是在https建立后,使用对称加密之后,如何验证双方的身份?

http与https的区别,参考自己以前写的博客-(要记得同步回博客中)

  • scheme不一样,一个是http,一个是https
  • Https是ssl加密传输,Http是明文传输
  • Https是使用端口443,而Http使用80
  • HttpsSSL+HTTP协议构建的可进行加密传输、身份认证的网络协议要比Http协议安全
  • Https协议需要到CA申请证书

列举下自己所知道的http的状态码?

1开头表示临时响应,还需要继续操作

2开头表示成功

3开头表示重定向,301表示永久移动,302表示临时移动

4开头表示请求错误,比如:400表示服务器不理解请求语法,403表示禁止访问,404未找到

5表示服务端错误,比如:500服务器内部错误、502错误网关、503表示 服务不可用,维护或者超载了

get和post方法之间有多少差异?get不会对后台数据进行修改,post会修改?

按照REST规范来说的话,get就是获取资源,不会有副作用,也就是这个操作是幂等的;post就是创建资源的意思,这个就不是幂等的。此外,put的含义就是replace,就是用来修改元素;delete就是用来删除。

但是如果,纯粹说http的话,这个就很宽泛了,我们说get不能有body、get是幂等、post有body、post不是幂等,这些规则都是可以被打破的,比如,post的参数可以放在queryString里、header里,当然也能放在body里,当然也可以每个部分放一点;get也能做成等非幂等的,甚至于,我们在日常使用的时候,post都能做成幂等,这就要求后台判断重复订单了。

以上文章参考自知乎上的文章(要记得同步回博客中)

TCP 下的流量控制的核心内容?-本质是不要把对方buffer打爆 。 对方已经很拥堵了,怎么控制呢?

java中静态方法能调用非静态方法吗?我从类对象角度讲的,但面试官感觉是要求从类加载的角度讲。

因为静态方法属于类,在类加载的时候,就被加载到方法区了,因此在类没有创建对象的时候后也是可以调用的。而非静态方法则得在类创建对象之后才存在。因此并不能这样调用。内容可以参考csdn的博客

Java中,有多层for循环,有个场景需要在最里面的for循环中跳出多个for循环继续执行下面的代码,怎么搞?

1
2
3
4
5
6
7
8
9
10
11
12
//比较简单的解决方案是:
public static void main(String[] args) {
loop: for(int i =0;i< 5;i++) {
for(int j = 0;j < 4;j++) {
if(j == 1) {
break loop;
}
}
}
}

//可以参考[博客园](https://www.cnblogs.com/fastfn/p/9777067.html)

创建fragment的时候,为什么官方不推荐构造方法,而要通过setArguments的方式?-为了考虑恢复。 了解到什么时候会有这种恢复的场景?-从前一个Activity返回到当前Activity。 Fragment 的两个adapter, 一个是 fragmentpageAdapter ,一个是 fragmentpageStateAdapter,他俩有什么区别?

FragmentPagerAdapter与FragmentStatePagerAdapter的区别:
FragmentPagerAdapter拥有自己的缓存策略,当和ViewPager配合使用的时候,会缓存当前Fragment以及左边一个、右边一个,一共三个Fragment对象;在每次切换页面的时候,是将fragment进行分离(transaction.detach),适合页面较少的fragment使用以保存一些内存,对系统内存不会有多大影响

FragmentStatePagerAdapter是PagerAdapter的子类,这个适配器对实现多个Fragment界面的滑动是非常有用的。当Fragment对用户不可见的时候,整个Fragment会被销毁(transaction.remove),只会保存Fragment的保存状态

参考刘强的博客赵凯强的博客

MeasureSpac 里面有三种模式,解释下?

参考以前写的面试题目第3题

handler的post和sendMessage有什么区别?-post是runnable,send是message,post的runnable最终会封装成 message,并且what默认为1,——面试官看起来不满足这个解释。

使用send时,由于是我们自己创建的Message,所以我们最终在handleMessage 方法中switch-case来处理Message。

使用post的方式时,我们post过去的是一个runnable,在post方法中,会创建一个 Message,并且将这个runnable赋值给Message的callback字段。之后通过 sendMessageDelay(msg,0) 来实现。

而我们知道,在Handler的dispatchMessage方法中,会首先判断msg.callback == null,如果不为null的话,就直接执行 msg.callback.run() 了;如果为null,则会判断 handler的mCallback字段是否为null,不null的话,直接调用 mCallback.handleMessage(msg) 将当前msg给处理了,并且直接return回去了;如果以上条件都不满足,则会调用我们通常覆写的 handleMessage() 方法,交由我们自己处理。

上面内容主要是看系统源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}


private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}


public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

算法:

给出两个有序的整数数组 imgimg,请将数组 img合并到数组 img中,变成一个有序的数组
注意:
可以假设 img数组有足够的空间存放 img数组的元素, imgimg中初始的元素数目分别为 imgimg

TX1

全程参与的项目有哪几个?

项目里面哪个对你成长帮助比较大,收获比较大?

遇到什么问题?为什么想着优化H5?如何优化H5页面的打开速度?是预创建 WebView 是吧?

项目中碰到什么难点,以及是怎么去攻克它的?

如果插件化要做好兼容性,应该怎么做?

如何保证App的安全?

有用 Service 开发吗?

你们的跨进程通信用的什么方式?你知道有哪些跨进程通信方式?这些跨进程通信方式各有什么优势或者特点,他们之间的差异是什么?

介绍下APK的启动过程——描述下进程启动、应用启动?启动过程哪些过程可能产生ANR ?

Apk 中有一些第三方的包 Jar,在应用启动过程中,这些 Jar 是怎么链接起来的 ?——Android会把这些 jar 打包成 dex , 那么这些 dex 是什么时候加载呢?

类似抖音功能遇到了什么难点?怎么解决的?——播放错乱,咋解决?

Android的源码看过多少?有编译过Android的源码吗?

adb命令会吗?我想查看手机的剩余电量,怎么拿? Linux的一些命令呢?root 和 Hook 了解多少?

NDK 开发有吗?

主要开发语言,Java 、Kotlin ?

ART 知道吗?与 Dalvik 的区别?GC有什么不同?

如果让你一个人开发App,能 Cover 住吗?

有做单元测试吗?

ALi

Java集合类有哪些?有什么特点? ArrayList、LinkedList、HashMap?

ConcurrentHashMap是线程安全?它是怎么实现线程安全的?

Java的线程有哪些状态?创建线程,调用它的start() 方法,这时候它处于什么状态?

Java 中处理多线程并发的方法有哪些?Synchronize 在底层是怎么保证加锁成功?Volatile 主要用在什么场景?

是否有用过 ThreadLocal ?看 Handler 源码时看过

线程的集中管理会用到线程池,有哪些比较关键的参数?

Activity声明周期?Activity A 到 Activity B ,二者经历的生命周期是怎样的?启动模式介绍下?日常开发时,有没有碰到要用某种启动模式的?

是否有多进程的场景?——做小程序的时候

Handler 的机制简单说一下?子线程发送消息,如何流转到主线程的?

讲下 View 的主要绘制流程?多层 View 和 Layout,怎么把这些操作传递下去吗?ViewRootImpl 有了解吗?

涉及到多进程的话,如何进行多进程间的通信?Messager 主要用在什么场景? AIDL 主要用在什么场景?Binder 的基本原理?为什么要引入Binder ,不用 Linux 的?

性能优化,页面秒开是从哪些方面做的?发现性能会有问题才做性能优化吧?是怎么分析性能?怎么检测内存泄漏?LeakCanary的原理?后来有没有思考有没有哪些内容可以做得更好?最耗时的点是啥,怎么去优化的?

做过小程序的开发,你们的小程序是自研的吗?有哪些权限限制?

RN的了解?

dayuwuxian

在目前所在公司主要负责什么?

为什么要重构网络框架?重构后有什么亮点?

网关不让用。并发token设计、数据解析 adapter 设计、日志系统设计。

OkHttp 里面有哪些比较关键的设计让你觉得挺好的?线程池为什么要这样设计?它有哪些 intercetor ,分别是什么含义 ?这些 interceptor 执行的顺序是什么?

网络防劫持是怎么做的?

设计一个类,有add、remove、dispatch 这 3 个方法,其中add 表示将 listener 添加进来, remove 表示将 listener 移除,dispatch 方法表示遍历目前的listener ,然后调用每个 listener 的 invoke 方法,请问这个类怎么设计?

设计的APM上线了吗,完成度怎么样?为什么要设计这么个APM ?怎么收集卡顿、ANR 呢?

有用kotlin吗?kotlin的 lazy 是怎么实现的?-有很多中模式。这些模式的原理是什么,比如Synchronize 模式的原理是什么?用过协程吗?

有用过 JetPack 吗?用过哪些?

https证书防抓包的原理是什么?证书是双向验证还是单向验证?破解包了替换掉证书还能不能行?怎么更新从后台下载来的证书?怎么做的?

https的请求过程是什么样的,与http有什么区别?

一大堆无序的数字,取出前k个最小的数字

道通科技

Kotlin的扩展函数的原理是什么?扩展函数 inline ?

了解 WebSocket 吗?自己用过 Socket 吗?

用过 NDK 吗? Android 中的 JNI 是怎么使用的?(使用 CMake)

极鑫科技

OkHttp 自带的那些 interceptor 是否可以换位置?

为什么要用 Builder (参数比较多的情况),那我用 set 不行吗?为什么要 Builder ?

说下 AMS

了解 PKMS 吗?

用过 RxJava 吗?怎么切换线程的?

文件下载实现,切片、断点续传

为什么要使用线程池

小算科技

Surface、Canvas 有了解吗? SurfaceFlinger 呢?

你开发的 SDK 如何防止别人调用你未公开的逻辑?

你们的广告是使用新进程吗?有做过进程保活吗?开启多进程能提升运行效率吗?(能,GC间隔长了,更多对象能在年轻代回收了,年轻代复制算法回收效率高,标记比清除的效率高)

万声音乐

你们的广告系统跨进程怎么通信?有哪些工具可以实现跨进程传递数据(Binder、socket、文件、广播)?除了AIDL 有没有更好的方式?Provider 有用过吗?Provider 的原理

Provider 是什么时候注册的?什么时候创建的?(Application 的 onCreate 之前)那么是在 attachBaseContext 之后吗?(是的)

事件相关的方法有哪几个?dispatchTouchEvent 和 onTouchEvent 的作用?为什么还要 onInterceptTouchEvent ?

卡顿怎么监控?(给Handler 的 Looper 设置日志打印器),这个方法名字叫啥?(忘了,Looper.getMainLooper().setMessageLogging()),这样有什么缺点吗?还有其他方式吗?(忘了每个方法插桩了)

你的 ANR 监控每秒让主线程改标记位,怎么判断ANR ?(5次没有改这个值),但是如果是广播,后台广播的超时时间是多长?那后台广播的 ANR 时长超过 5s 的话 ,你这个 5s 判断 ANR 是否会有误判?ANR 之后,你什么时候 dump 现场信息

Choreographer 了解吗?怎么记录 FPS (每秒绘制的帧数)?(使用 Choreographer.getInstance().postFrameCallback,然后计算每秒多少帧)

你的APM 本身出了问题怎么办? 你APM 会不会监听你自己 APM 本身的代码?

kotlin 中在一个类的内部写一个类,加上 inner 和 不加 inner 有什么区别? inner 类有什么问题吗?(生命周期不一致,可能有内存泄漏风险,因为内部类持有外部类引用)

kotlin inline 的作用是什么?是怎么提高效率的?(减少方法调用,因为方法需要栈帧记录局部变量、操作树栈、方法出口等,还需要切换,所以浪费存储空间和cpu操作)

Kotlin 的协程的挂起是个什么概念?比线程阻塞有什么区别?怎么切换线程的?

有用过 Kotlin 的 Flow 这些吗?

第3章: 冷启动代码修复

热修复的根本原理是Native层方法替换,所以当类结构变化(如增减method/field)时热部署模式会受限。但冷部署可以突破这种约束,可以作为热部署的补充。

冷启动类加载原理

冷启动方案概述

项目 QQ空间 Tinker
原理 为解决Dalvik情况下 unexpected dex problem 异常而采用插桩,单独放一个帮助类在独立的dex让其他类调用,防止类被打上 CLASS_ISPREVERIFIED。 最后加载补丁dex得到一个Element对象插入到 dexElements数组的最前面 提供差量包patch.dex,然后将patch.dex与应用的 classes.dex 合并成一个完整的 dex,完整的dex加载构建Element 然后整体替换掉旧的 Elements 数组
优点 产物小,灵活 补丁包小,Dalvik情况下不影响性能,ART环境下也不存在必须包含父类/引用类的情况
缺点 Dalvik 下影响类加载性能 dex合并内存消耗在 vm heap 上,容易导致 OOM

插桩实现的前因后果

如果直接把补丁类打入补丁包中而不做任何处理的话,在类加载的时候会产生异常并退出,来分析下异常产生的原因:

加载一个dex文件时如果不存在 odex 文件,那么就会执行 dexopt 操作,这个操作里面会调用到 verifyAndOptimizeClass 执行真正的 verify/optimize 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(doVerify) {
if(dvmVerifyClass(clazz)) {//执行类的verify
//类被打上 CLASS_ISPREVERIFIED 标志
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
...
}
}

if(doOpt) {
if(...) {
} else {
...
dvmOptmizeClass(clazz, false);
//类被打上 CLASS_ISOPTIMIZED 标志
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISOPTIMIZED;
}
}

第一次安装 APK 时,会对原dex做 dexopt ,假如APK只有一个dex,dvmVerifyClass(clazz) 返回true,然后 APK 中所有的类都会被打上 CLASS_ISPREVERIFIED 标记。接下来执行 dvmOptimizeClass ,类接着被打上 CLASS_ISOPTIMIZED 标志。

  • 上述的 dvmVerifyClass 目的是防止校验类合法性被篡改,此时会对类的每个方法进行校验,这里只需要知道如果类所引用到的类和当前类都在同一个dex中的话,就会返回为true

  • dvmOptmizeClass : 类优化,这过程会将部分指令优化成虚拟机内部指令,提升了方法的执行效率。

现在假定 A 是补丁中的类,所以 A 肯定在一个单独的 dex 中,这时候类B 引用到补丁类 A,所以会尝试解析类 A。但是由于 B 是已经被打上了 CLASS_ISPREVERIFIED,调用的类 A 又与 B 不在同一个 dex ,因此会报 dvmThrowIllegalAccessError 异常。

为了解决这个问题,一个单独的无关类被放在一个单独的dex中,原 dex 中所有的类构造函数都引用这个类,一般的解决方案是侵入dex打包流程,利用 .class 字节码修改技术。插桩由此而来,根据前面的介绍,原dex 所有的类都没有 CLASS_ISPREVERIFIED 标志,因此不会报这个异常。

但是,插桩会给类加载效率带来比较严重的影响,如果没有打上 CLASS_CLASS_ISPREVERIFIED / CLASS_ISOPTIMIZED 标志,那么累的校验和优化都将在类初始化阶段进行。正常情况下类的校验和优化都仅在APK 第一次安装执行 dexopt 阶段进行,单个类加载耗时并不多,但是同一时间加载大量类的情况下,这种耗时就会放大。因此启动的时候容易白屏,这是没法容忍的。

避免插桩的 QFix 方案

它的思路是,在dexopt 后,反编译 dex 为 smali 修改代码避免异常,但是由于是在 dexopt 后绕过的,dexopt 会改变原有的很多逻辑,这可能会导致比较严重的bug。因此,最终会采用自研的全量 dex 方案。

Art下冷启动实现

为了解决 Art 下类地址写死的问题,Tinker 通过 dex merge 成一个全新的 dex 整体替换掉旧的 dexElements 数组。

Dalvik 虚拟机尝试加载一个压缩文件的时候,只会加载 classes.dex ,如果压缩文件中有多个 dex, 其他 dex 文件会被直接忽略掉;而 Art 虚拟机则不一样,优先加载 primary dex (也就是 classes.dex),后续会加载其他 dex 。所以,在 Art 环境下,补丁类只需要放到 classes.dex 中即可!后续出现在其他 dex 中的 “补丁类”是不会被重复加载的,所以 Art 下冷启动解决方案:

将补丁 dex 命名为 classes.dex ,原来的dex依次命名为 classes(2,3,4…),然后一起打包为一个压缩文件,通过 DexFile.loadDex 得到 DexFile 对象,最后整体替换老旧的 dexElements 数组即可。

该方案和Tinker 的方案对比如下:

Art冷启动方案与Tinker对比

其他方面

DexFile.loadDex 把 dex 文件解析并加载到 native 内存的时候,如果dex 不存在 odex ,则会生成一个优化的 odex (Dalvik 和 Art 都会),所以虚拟机最后执行的是 odex。现在假如补丁 dex 足够大,那么 生成 odex 是很耗时的。不过上面的冷启动方案,在 Dalvik 下影响比较小,因为 loadDex 只是操作补丁包;但是在Art 环境下影响比较大了,因为 loadDex 是补丁 dex 和原有 dex 合并的完整包,所以是比较耗时的。

为了解决这个问题,要把 loadDex 当做一个事务,如果中途被打断,那就删除 odex 文件,重启的时候时候如果发现 odex ,loadDex 完之后 ,反射注入/替换 dexElements 数组,实现打包。如果不存在 odex 文件,那么重启一个子线程 loadDex ,重启后再生效。

为了补丁包安全,要对补丁包进行签名校验,防止整个补丁包被篡改,但是虚拟机执行的是 odex ,因此还需要对 odex 文件进行 MD5 校验,如果匹配,则直接加载,否则,重新生成 odex 文件,防止 odex 文件被篡改。

完整的方案考虑

冷启动修复方案机会可以修复任何场景下的代码缺陷,但是注入前被加载的类(如: Application 类)肯定是不能修复的。所以我们将其作为保底方案,在没法应用热部署或者热部署失败的情况下,最后都会应用代码冷启动方案,所以我们的补丁是同一套的。具体实施方案对 Dalvik 和 Art 下分别做了处理:

  • 在Dalvik 下采用自研的全量 dex 方案

  • Art 本身支持多 dex,我们仅仅把补丁dex 作为主dex (classes.dex) 加载而已

冷启动方案限制

重新认识多态

实现多态的技术一般叫做动态绑定,是指在执行期间判断所引用对象的实际类型,根据实际类型调用相应方法。多态一般指的是非静态非私有方法的多态,field 和 静态方法都不具有多态性。

虚拟机在加载类时回味其生成一张 vtable 表,里面存放当前类所有 virtual 方法的一个数组,当前类和所有继承父类的 public/protected/default 方法就是 virtual 方法,因为 public/protected/default 方法是可以被继承的, private/static 不再这个范畴,因为不能继承。具体操作:

  • 整体复制父类 vtable 到子类的 vtable

  • 遍历子类的 virtual 方法集合,如果方法圆形一致,说明是重写父类方法,那么在相同索引位置处,子类重写方法覆盖掉 vtable 中父类的方法

  • 若方法原型不一致,那么把该方法添加到 vtable 末尾

所以就有:假如父类A的 vtable[0]=A.a_t1 ,vtable[1]=A.a_t2 方法,那么类B可能是: vtable[0]=B.a_t1, vtable[1]=A.a_t2, vtable[2]=B.b_t1 。

至于为什么field 和 static 方法为什么不具多态性质,主要是因为: 在这场景下,是从当前变量的引用类型而不是实际类型中查找,如果找不到,再去父类递归查找。

限制

预先储备多态知识,来看如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo {
public static void test_addMethod() {
A obj = new A();
obj.a_t2();
}
}



class A {
int a = 0;
//补丁中新增 a_t1 方法
void a_t1() {
Log.d("Sophix", "A a_t1");
}

void a_t2() {
Log.d("Sophix", "A a_t2");
}

}

也就是修复之后,A 类中新增了 a_t1 方法,Demo 类不做任何修复,测试发现应用补丁后,test_addMethod() 方法执行输出了 Sophix:A a_t1 ,表明在执行 t2 方法的时候,实际调用了 t1 方法!

前面说了,dex 文件第一次加载的时候会有 verify 和 optimize 过程,优化过程会把 invoke-virtual 重写为 invoke-virtual-quick,后面跟的立即数就是该方法在类 vtable 中的索引值,这样执行更高效。所以我们知道了问题的根源了:打包前类A的 vtable 值为 vtable[0]=A.a_t2,打包后类 A 新增了 ,A.a_t1 方法,那么类A的vtable 变为了: vtable[0]=A.a_t1 ,vtable[1]=A.a_t2,但是 test_addMethod 中执行 obj.a_t2() 已经被优化为了 A.vtable[0],所以修复前调用的是 a_t2方法,修复后变成了调用 a_t1 方法了,导致方法调用错乱!

终极解决方案

由上面可知,由于多态影响,QFix方案会遇到问题,我们最后的希望就只能依靠类似Tinker的完整dex解决方案,并且利用Google 已经开源的 DexMerge 方案,把补丁 dex 和原 dex 合并成一个完整的dex 似乎是可行的。但是,这样还是不够的,多dex下如果DexMerge抛出 65535 方法数异常,DexMerge 会导致内存风暴,内存不足的情况下容易更新失败,完整的 dex 合成需要在移动端进行,实现复杂。

Dalvik 下完整dex方案的新探索

冷启动类加载修复

冷启动类加载修复最早实现方案是 QQ空间提出的 dex 插入方案:把心dex插入到 ClassLoader 索引最前端,这样在加载一个类时,会优先查找补丁中的类。但是,这类插入dex的方案都会在 Dalvik 虚拟机下遇到 pre-verify问题。腾讯三大热修复方案解决该问题的思路如下:

  • QQ空间插桩,每个类中插入一个来自其他 dex 的 hack.class ,使得所有类都无法满足 pre-verified 条件

  • Tinker 的方案是合成全量的 dex ,这样所有类都在全量 dex 中解决,从而消除类重复带来的冲突

  • QFix 的方式是获取虚拟机中的某些底层函数,提前解析所有补丁类,以此绕过 pre-verify 检查

以上三种方案,插桩会侵入打包流程,并且插桩添加臃肿的代码,不优雅;QFix 方案需要获取底层虚拟机的函数,不稳定可靠。并且,插桩和 QFix 方案都不能新增 public 函数,具体原因后续讲解。 Tinker 需要从 dex 方法和指令维度全量合成dex ,虽然节省空间,但是性能消耗比较严重。

一种全新的全量 Dex 方案

一般来说,合成完整dex,思路就是把原有 dex 和补丁包里的 dex 重新合并成一个,然而我们的思路是反过来的。可以这样考虑,既然补丁中已经有变动类了,那么只要在原先基线包里的dex中,去掉补丁中也有的类,这样,补丁+去除了补丁类的基线包,不就等于新 App 中所有类了吗?

参照Android原生的 multi-dex 理解: multi-dex 是把一个 APK 里用到的所有类拆分到 classes.dex、classes2.dex 、classes3.dex 等,每个dex 只包含了部分类定义,但是单个 dex 也是可以加载的,只要把所有dex 都加载进去,本 dex 中不存在的类就可以在运行期间在其他 dex 中找到。

基线 dex 在去掉了补丁中的类后只包含不变的类了,这些不变的类在用到补丁中的新类时会自动找到补丁 dex ,补丁dex 中的心累在需要用到不变的类时也会找到基线 dex 的类。这样做的好处是: 基线包里面不使用补丁中类的仍然可以按照原来的逻辑做 odex,最大限度地保证了 dexopt 效果。同时,避免了传统dex 合成遇到的 65535 方法数超了的问题、对dex破坏性重构的问题。

现在,问题简化为如何在基线包中去掉补丁包中包含的所有类!。需要注意的是:并不是要把某个类的所有信息都从dex移除,因为这样做的话,会导致dex 的各个部分都要发生变化。我们只需移除定义的入口,解析这个dex的时候找不到这个类的定义即可。最大限度地减少修改。

具体实现方案是:在 dex 的 pHeader->classDefOff 偏移处 ,把相应的 ClassDef 移除即可

对于 Application的处理

Application 是整个App入口,在进入到替换的完整 dex 之前,一定会通过 Application 代码,所以,Application 一定是加载在原来的 dex 里面的,只有在补丁加载后使用的类,会在新的完整 dex 里面找到。在补丁加载后,如果 Application 类使用其他新 dex 里的类,由于不在同一个 dex 里, 而如果 Application 被打上了 pre-verify 标记,这时候就会抛出以前说的异常。

解决方案:在 JNI 层清除掉它的 pre-verify 标记即可

但这样还会引出问题:Dalvik 发现某个类没有 pre-verifid,就会在初始化这个类的时候做 verify 操作,对这个类使用到的类都要进行 opt操作。此时补丁还未进行加载,所以就会导致提前加载到原始dex中的类,当补丁加载完毕之后,当这些已经加载的类用到新dex 中的类,并且又是 pre-verified 时就会报错。这种问题在单 dex 情况下不会出现,但是多 dex 情形就会发生,解决办法如下:

  • 让 Application 用到的所有非系统类都和 Application 位于同一个 dex ,这样可以保证 pre-verified 被打上,而在补丁加载完成后,再清除 pre-verified 标志

  • 把 Application 里面除了热修复框架代码外的其他代码都剥离到一个其他类中,这样使得Application 不会直接用到过多非系统类,这样,保证这个单独拿出来的类和Application 处于同一个 dex 的概率还是很大的。如果要更保险,Application 可以采用反射的方式访问这个单独的类,这样彻底把 Application 和 其他类隔绝。

其他方案针对 Application 的解决办法:

  • Tinker 方案是在 AndroidManifest.xml 中要求开发者写Tinker 的 Application ,真正的 Application 在 初始化 TinkerApplication 时作为参数传入,这样,TinkerApplication 会接管真正的 Application。如果对 Application 有更多扩展的话,开发者接入成本也是比较高的。

  • Amigo 的方案是在编译过程中,用 Amigo 自定义的 gradle 插件将 App 的 Application 换成 Amigo 自己的 Application,并且保存真正的 Application 的name,在问题修复完成之后,再反射真正的 Application 之后调用其 attach(context) 方法。虽然开发者无感,但是这种系统反射本身是有一定风险的。

入口类与初始化时机

如果要使热修类之前使用的其他类最少,只能放在Application中。那么,放在 Activity 中是不是也可以呢?当然,如果你没有 Application ,放到Activity中似乎没什么问题。但是,如果AndroidManifest 中注册了ContentProvider ,那么 ContentProvider 的 onCreate 方法是先于 Activity 的onCreate 调用的(事实上比Application的onCreate 都早)。这就可能导致某些类被提前引入了。

如果放在Application中又有2中选择:

  • 放attachBaseContext中,这是 Application 中最早被执行的代码了,当然没问题,但是App申请的权限还没授予完成,会遇到无法访问网络之类的问题,因此无法下载新补丁

  • 放在 onCreate 中,和Activity 一样,也会晚于 ContentProvider,这就需要自己保证没有 ContentProvider 以及第三方库也没有

注意:真实的启动顺序: Application.attachBaseContext->ContentProvider.onCreate->Application.onCreate-> Activity.onCreate

底层热替换的原理

App 启动到一半的时候,所有需要发生变更的类已经被加载过了,Android 中无法对一个分类进行卸载的。腾讯系的方案是让 ClassLoader 去加载新类,因此只有在下次 App 重启的时候还没运行到业务逻辑之前抢先加载补丁中的新类

阿里系的 Andfix 采用的方法是直接在 Native 层替换了已经加载的类的方法。原理是:每一个 Java 方法在 Art 虚拟机中都对应一个 ArtMethod ,ArtMethod 记录了这个 Java 方法的所有信息,包括所属类、访问权限、代码执行地址等,可以获取 ArtMethod 的起始地址,然后强制转化为 ArtMethod 指针,从而对其包含的所有成员进行修改

但是,上述做法会导致兼容性问题,因为Android 是开源的,所以手机厂商可能对 ArtMethod 的结构体进行修改,举个例子:

在 Andfix 中替换 declaring_class_ 的地方:

smeth->declaring_class_ = dmeth->declaring_class_

由于 declaring_class_ 是 官方 ArtMethod 的第一个成员,因此它和以下代码等价:

(unit32_t) (smeth + 0) = (uit32_t) (dmeth + 0)

但如果手机厂商在 ArtMethod 的结构体 declaring_class_ 前面添加了 additional_ 字段,那么 additional_ 就成了第一个成员,所以 smeth + 0 在这台设备上实际就成了 additional_,代码含义就变化了。

突破底层差异

起始,Native 层面替换思路,其实就是替换 ArtMethod 的所有成员。那么,如果不构造 ArtMethod 中的各个成员字段,只要把 ArtMethod 作为整体替换,这不就可以了吗?示意图如下:

ArtMethod整体替换对比

所以,一系列繁琐替换:

繁琐替换

都可以缩写为:

1
memcpy(smeth, dmeth, sizeof(ArtMethod));

即使手机厂商把 ArtMethod 改得面目全非都无所谓,但是 sizeof(ArtMethod) 获取 ArtMethod 的 size 大小不容易,很容易导致替换区域超出边界或者没替换到

由于 ArtMethod 存在一个 ArtMethodArray 中,并且多个 ArtMethod 紧密排列,所以一个 ArtMethod 的大小刚好就是两个相邻 ArtMethod 的起始地址的差值,就这样巧妙获取到了 ArtMethod 的大小。所以,后续不管 ArtMethod 结构怎么变化,只要他们在 ArtMethodArray 中还是以线性排列,就能直接适配。

访问权限问题

看到这里可能会疑惑,我们只是替换了 ArtMethod 的内容,但是**补丁方法所属的类和原有方法所属的类,是不同的类型,被替换的方法有权限访问这个类的其他 private 方法吗?

需要注意的是,在构造函数调用同一个类的私有方法 func 时,没有做任何权限检查,也就是说,这时即使把 func 方法偷梁换柱,也能直接跳过去正常执行而不报错。可以推测在dex2oat 生成 AOT 代码的时候是有做一些检查和优化了,由于在 dex2oat 编译机器码时确认了两个方法同属一个类,所以机器码中就不存在权限检查。

相同包名的问题

但是,你会发现补丁中的类在访问同包名下的类时,会爆出访问权限异常。这个问题是如何产生的呢?

虽然 com.path.demo.BaseBug 和 com.path.demo.MyClass 是同一个包 com.path.demo 下的,但是由于我们替换了 com.path.demo.BaseBug.test 方法,而用于替换的 BaseBug.test 方法是从补丁包 Classloader 中加载的,与原有的 base 包就不是同一个 Classloader 了,就这样,导致两个类无法被判别为同包名。

解决办法就是设置新类的 Classloader 为原来类的就可以了,代码如下:

1
2
3
Field classloaderField = Class.class.getDeclaredField("classLoader");
classloaderField.setAccessible(true);
classloaderField.set(newClass, oldClass.getClassLoader());

反射调用非静态方法产生的问题

当一个非静态的方法被热替换后,在反射调用这个方法时,会抛出异常。例子如下:

反射非静态方法抛出异常

这里面,expected receiver 的 BaseBug 和 got 的 BaseBug 不是同一个类,虽然路径名字都一样,前者是被热替换的方法所属的类,后者是作为被调用的实例对象 bb 所属的类,是原有的 BaseBug,二者是不同的。因为在执行 invoke 的时候会检查调用对象是否是类的一个实例,只有是它的实例才能调用。具体的解决办法我们采用另一种冷启动机制应对

为什么静态方法不会有问题呢?因为如果是静态方法,是在类级别直接调用的,就不需要接受对象实例作为参数,所以就没有这方面的检查。

即时生效带来的限制

这种直接在运行时修改底层结构的热修复方案,都存在一个限制:只能支持方法的替换,新增/减少方法或者增加/减少成员字段,都是不适用的。因为一旦补丁类中出现了方法的增加或者减少,就会导致这个类以及整个 Dex 的方法数变化,方法数的变化会伴随方法索引的变化,这样,在访问方法时无法正常地索引正确的方法。

字段的变化也是一样的,所有字段的索引都会发生变化。更严重的是,如果程序运行中某个类突然增加了一个字段,那么对于原有这个类的实例,他们还是原来的结构,这是无法改变的;而新方法使用这些旧的实例对象时,访问新增字段就会产生不可预期的结果。不过,新增一个完整的、原有包里面不存在的新类是可以的

编译期与语言特性的影响

内部类编译

在修改外部类的某个方法时,最后打出的补丁竟然提示新增了一个方法!所以有必要了解内部类在编译期是怎么工作的,首先要知道内部类在编译期间会被编译为外部类一样的顶级类

内部类和外部类互相访问

既然内部类实际上和外部类一样都是顶级类,那应该私有的 method/field 是无法被访问到的,事实上外部类为了访问内部类私有 域/方法 ,编译期间会为内部类自动生成 access$ 数字编号相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseBug {
public void test(Context context) {
InnerClass inner = new InnerClass("old apk");
Toast.makeText(context, inner.s, Toast.LENGTH_SHORT).show();
}


class InnerClass {
private String s;
private InnerClass(String s) {
this.s = s;
}
}
}

为了能在外部类实现 inner.s 这种调用,编译器会自动为 InnerClass 类合成 access$100 方法,这个方法简单返回私有域 s 的值。同样,如果此时匿名内部类需要访问外部类的私有属性/方法时,外部类也会自动生成access$ 相关方法供内部类访问**。

所以,出现问题的时候应该是出现了这样一种场景:打补丁前test方法没访问 inner.s 打补丁后访问了,那么补丁包中就会新增 access$100 方法!

解决方案

既然知道是因为产生了自动生成的方法导致的,那么我们阻止方法的自动生成就好了:

把外部类、内部类的所有 method/field 的私有访问权限改为 protected 或者 public 或者默认访问权限

匿名内部类特殊情形

匿名内部类其实也是个内部类,所以也会有上面内部类的影响,但是新增一个匿名类(补丁热修复模式允许新增类),并且符合上述内部类解决方案的要求,但是还会提示 method 新增,所以接着了解匿名内部类。

匿名内部类名称格式一般是: 外部类$数字编号,后面的数字编号是编译期根据匿名内部类在外部类出现的先后关系,依次累加命名的。看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DexFixDemo {
public static void test(Context context) {
/*new DialogInterface.OnlickListener() {
@Override
public void onClick(){
Log.d("aa", "bb");
}
}*/

new Thread("thread-1") {
@Override
public void run(){
Log.d("xx", "yy");
}
}.start();
}
}

修复后新增 DialogInterface.OnlickListener 这个匿名内部类,但是最后补丁工具发现新增 onClick 方法!具体原因:修复之前只有一个 Thread 匿名内部类,此时它的名称应该是 DexFixDemo$1 ,然后打补丁后在 test 方法中新增了 DialogInterface.OnlickListener 匿名内部类,由于位置关系,它的名称变成了 DexFixDemo$1,而 Thread 的匿名内部类的名称变为 DexFixDemo$2 了 !所以,前后2个 DexFixDemo$1 类进行对比差异,就会完全乱套。同理,减少一个匿名内部类也存在同样的情况

匿名内部类解决方案

新增或者减少匿名内部类,实际上是无解的,因为补丁工具拿到的是已经编译后的 .class 文件,根本没法区分 DexFixDemo$1 或者 DexFixDemo$1 类。所以,应当避免插入一个新的匿名内部类。当然,如果匿名内部类是插入到外部类的末尾,那么是允许的!

有趣的域编译

热部署中也不支持 的修复,这个方法会在 Dalvik 虚拟机中类加载的时候进行初始化调用,Java中本身没有这个方法,是编译器自动生成的。静态的 field 的初始化和静态代码块实际上就会被编译在 方法中。也就是热修复不支持静态代码块和静态field **。顺便说一下,静态代码块和静态域初始化在 中的先后关系就是二者出现在源码中的先后关系**。

非静态域与非静态代码块被编译器翻译在 默认无参构造函数中,域 和 代码块出现在 的顺序也和源码中的顺序一致。 是在对象初始化的时候被调用的,简单来说就是创建一个对象就会对这个对象进行初始化。

域编译热部署解决方案

前面说了, 方法不支持热部署,即静态 field 和静态代码块都不支持热部署。非静态的 field 和 代码块的变更会被编译到 中,热部署模式下会被视为一个普通方法的变更,因此热部署是不受影响的。

final static 域编译

开始很自然地认为 final static 是一个静态域,因此自然认为会被编译到 方法中,所以热部署也是不支持的,但是测试发现,final static 修饰的基本类型或者 String 常量类型不会被编译到 中!static 和 final static 修饰的 field 的区别如下:

  • final static 修饰的原始类型和String类型域(非引用类型,如: “haha”),并不会被编译在 方法中,而是在类初始化执行 initSFields 方法时初始化赋值
  • final static 修饰的引用类型(如: new String(“haha)),仍然在 中初始化

所以我们平时看到的说如果一个field是常量,那么尽量推荐使用 static final 作为修饰符,这句话应该是不完全正确的,得到优化的仅仅只是原始类型和String类型域(非引用类型),如果是引用类型不会有任何优化。那到底这个优化过程是怎样的呢?如果只是 static ,那在获取 field 之前,还需要判断是否有解析所属的类,如果没有还要解析这个类,之后才能获得;但是 static final 就可以通过立即数就能获得(注意,对于字符串来说,获取的立即数只是字符串常量的索引,还需要去通常说的“字符串常量“区获取)。

final static 热部署方案

  • 对于 final static 修饰的基本类型或者非引用类型的 String 域,由于**在编译期间引用到基本类型的地方被立即数替换,所以在热部署模式下,最终所有引用到该 final static 域的方法都会被替换,所以是可以做热部署方案的。
  • 修改final static 引用类型域,是不允许的

有趣的方法编译

除了以上内部类和匿名内部类可能造成 method 新增外,还有使用了混淆方法编译导致的内联和裁剪,也会导致最终 method 的新增或者减少。

方法内联

实际上好几种情况会导致方法被内联:

  • 方法没有被任何地方引用,会被内联掉
  • 方法足够简单,比如方法只有一行代码,那么任何调用该方法的地方斗殴会被该方法的实现替换掉
  • 方法只被一个地方引用,就会被内联

方法裁剪

比如如下方法:

1
2
3
4
5
public class BaseBug {
public static void test (Context context) {
Log.d("aa", "bb");
}
}

但是查看混淆 mapping.txt 映射文件:

1
2
com.demo.BaseBug -> com.demo.a;
void test$faab20d() -> a

因为context 没有被使用,所以 test 方法的 context 参数被裁剪,混淆任务首先生成 test$faab20d() 裁剪后的无参方法,然后再混淆。那怎么才能不发生裁剪呢?不让编译器在优化的时候认为引用了一个无用的参数就好了,方法很多,介绍一个最有效的方法:

1
2
3
4
5
6
public static void test(Context context) {
if (Boolean.FALSE.booleanValue()) {
context.getApplicationContext();
}
Log.d("aa", "bb");
}

注意,这里不能用基本类型 false,必须用包装类 Boolean,否则if语句也会被优化掉!

内联裁剪的热部署解决方案

实际上只要在混淆配置文件中加上 -dontoptimize 项,就不会做方法的裁剪和内联。

switch-case语句编译

在switch-case情况下,case 后面的id 可能替换不掉的情况。因为switch-case编译时会做优化,比如,case后面的值是连续的 1,3,5 ,那就会编译成 packed-switch 指令;如果是 1,3,10 这种没什么规律连续的,就编译为 sparse-switch。编译器会自行确定怎样才是连续的。

热部署方案

如果 case 被编译成 packed-switch 指令,那么如果不做任何处理的话,就会遇到资源id替换不完全的问题。解决方案也很简单,修改 smali 反编译流程,碰到 packed-switch 指令就强转为 sparse-switch指令,然后做资源ID的暴力替换,最后再回编译 smali 为 dex 。但是这也会导致打补丁变得慢了些。

泛型编译

泛型的使用也会导致 method 的新增。Java 语言的泛型基本完全在编译器中实现,由编译器执行类型检查和类型推断,然后生成普通的非泛型字节码。虚拟机是完全无感知的。

有泛型的情况下,子类真正重写基类方法的是通过编译器桥接实现的。桥接方法的内部实现就是调用子类重写的方法。

泛型的热部署方案

如果我们的代码由 class B extends A 变成了 B extends A ,那么就可能增加对应的桥接方法,此时新增了方法,就只能通过冷部署修复。

Lambda表达式编译

Lambda 表达在编译期间会为外部类生成一个static 类型的辅助方法,该方法内部实现 Lambda 的逻辑。所以导致方法的新增。在这个过程中会在外部类中生成一个非静态的内部类。

Lambda 表达式热部署

由上面可知,修改源代码中的Lambda 也会出问题:

  • 如果基线包Lambda没有访问非静态field/method ,那么它就不会持有对外部类的引用;
  • 如果后续补丁包访问外部类非静态 field/method,则会持有外部类引用,会新增field,热修复失败

所以总结如下:

  • 增加或者减少Lambda 表达式会导致类方法错乱,导致热部署失败
  • 修改Lambda 表达式可能导致新增 field ,导致热修复失败

访问权限检查对热修复的影响

如果当前类和实现接口(父类)是非 public 时,同时负责二者的 ClassLoader 不一样的情况下,直接检查失败;所以热修复如果不处理这里,会导致类加载阶段报错,因为我们当前的代码热修复方案是基于新的 ClassLoader 加载补丁类。

如果补丁类中存在非 public 类的访问或非 public 方法或域的调用,那么会加载失败,并且在补丁加载阶段是检测不出来的,只有在运行阶段才会报错。

热修复方案

如果出现这种情况只能冷启动重启生效,因为类已经加载过了。

引申

关于内部类

静态内部类/非静态内部类的区别: 后者会持有外部类的引用,前者不会。在 smali 中的表现就是非静态内部类会在编译期间自动合成 this$0 域,这个 this$0 就是外部类的引用。

关于proguard

preverification step : 针对 .class 文件的预校验,在 .class 文件中添加 StackMa/StackMapTable 信息,这样在执行类校验阶段会省去一些步骤,这样类加载更快一些。但是Android 虚拟机执行的是 .dex 文件,会把所有的 .class 转为 .dex ,所以这个选项是没有任何意义的,反而降低打包速度。所以一般禁止这个选项: -dontpreverify 这一项

为什么需要热修复?

  • 重新发版代价太大,用户更新还不及时
  • bug修复不及时,用户体验差
  • 插件化?那就很小的改动也要发布一个模块
  • Hybrid方案?需要将经常变更的业务以H5独立出来,但是如果无法转为H5,bug还是无法修复,并且需要开发人员新增技术栈
  • 热修复对用户的流量小,影响小,损失降到最低

从本质理解热修复的实现

AndroidManifest 出现bug是无法修复的,因为它是由系统进行解析的,解析过程不会访问补丁包,因此如果想要增加四大组件,通常来说是不可以直接添加的。通常的做法是预先在安装包AndroidManifest 中埋入代理的组件

代码如何修复,所有的java代码最终都编译为 class.dex 格式文件,因此,任何的热修复方案,都需要在补丁包中包含一个新逻辑的 dex 文件,并且改变执行流,引导执行到新 dex 文件。

资源的修复,必须想办法把原油安装包替换为新的资源包,而有些资源,比如桌面图标、通知栏图标以及 RemoteView 之类的资源,是由系统直接解析安装包的资源得到的,对于这类资源,任何热修复方法都无法进行资源替换和修复。

so的修复应该是最明确的,Android 系统中所有的 so 库都是由 System.load 进行加载,因此只要找到办法在加载的时候优先加载补丁包的 so,就能够进行底层代码替换了。

发展历程

阿里最开始的 Dexposed 框架基于 Xposed 框架的 Java Method Hook 技术,但是对底层 Dalvik 结构过于依赖,最终无法继续兼容 Android 5.0 以后的 ART 虚拟机,后续作罢。

后来提出的 Andfix 同样是底层结构替换的方案,做到了 Dalvik 和 ART 环境的兼容。再者,还有基于 Andfix 阿里百川 HotFix ,但是都是基于底层固定结构,使用有很多限制,只提供代码层修复,不支持资源和so层修复。

行业内著名的还有: 腾讯空间超级补丁技术、微信的Tinker、饿了么的 Amigo、美团的 Robust等。

最终,阿里推出了 Sophix 方案,支持 代码修复、资源修复、so修复。它的核心理念就是 非侵入性。不修改打包流程,也不插入任何 AOP 代码。唯一需要的就是 初始化和请求补丁两行代码。

代码修复

代码修复主要有2大方案:

  • 阿里的底层替换方案: 立即见效,加载也轻快,但是限制很多。
  • 腾讯系的类加载方案:需要冷启动见效,修复范围广,限制少。

背景

为了防止Fiddler抓包,实现更进一步的安全。以及,如果后台证书变更,能够更方便地实现替换。

证书获取

assets目录下内置默认的crt证书,第一次使用App的话,则sp中还没有证书,就判断assets中的证书有效期,如果有效,先使用该crt,并且同时请求后台获取最新的证书。

由于本地有crt证书,并且自带有tag,后台根据tag判断是否需要下发新的证书;请求后台证书这个接口,通过114或者119获取到ip,之后将host设置到请求的header中,(为什么要这样做,是因为有些服务会校验host是否正确的;此外,ip直连之后,只能直达主机,如果一个主机承载多个服务,没有host没法区分服务)这样以ip直连的方式请求该接口。注意,不管后台有没有证书返回,这里都可以返回服务器当前时间,防止用户更改手机时间,不过这种情形其实更改手机时间是没什么用的。

新证书验证

请求下来的证书会存在sp中,下次启动App时,从sp中读取缓存证书,如果有效,则使用sp缓存的证书。

校验过程: 首先,新证书自己先一级级验证通过,如果自己都验证不通过,则肯定非法;其次,验证本地证书和新证书是否包含当前请求的h-o-s-t 来确认证书验证是否合法;最后,通过比本地证书和当前请求返回的证书的公钥,只要新证书中含有旧证书中的某个公钥,就说明通过验证;

当然,你也许会说如果换了CA厂家,那么可能新证书里面压根就不包含旧的证书中的公钥了,那我只能说,如果换了新证书了必然要求强制升级了。

Android Jetpack 简介

Jetpack 并不是一个框架或者组件,它是一套库、工具和指南的集合。其中很多组件都不是新开发的,而是很早就存在的,Jetpack 有个非常重要的特性:消除样板代码,此外,它还有加速开发、构建高质量的强大应用等特性。

Androidx也是属于 Jetpack

Jetpack 分类

Android Jetpack 分类为 4 种:

  • 架构组件(Architecture):包括Data binding、Lifecycle、LiveData、Room、WorkManager、ViewModel、Navigation、

    Paging

  • 基础组件(Foundationy):提供横向功能,比如向后兼容、测试、安全以及Kotlin支持等。如:Android KTX,AppCompat

  • 行为组件(Behavior):帮助开发者开发的应用与标准的Android服务(通知、权限、分享等)相集成。如:CameraX、Download Manager、通知、分享等

  • 界面组件(UI):包括常见的效果和内置动画。如: Fragment、Emoji、布局、调色板等