目录

1. 数据结构

1.1 常用数据结构有哪些

1.2 HashMap、ArrayList、LinkedList、堆、栈、队列

HashMap

ConcurrentHashMap

1.2.1 堆、栈是什么?怎么实现栈?

1.2.2 数组和列表的区别

1.2.3 ArrayList与LinkedList的区别、选择

1.2.4 ArrayList原理

2. 语言

2.1. JAVA

2.1.1. JVM

双亲委托机制

内存模型

GC生命周期

垃圾回收算法

2.2. Kotlin

2.2.1. 怎么理解空安全检测,安全检测一定能避免这些问题吗?

4. 内存

4.1. 内存泄漏

4.1.1. 表现、原理、怎么避免、怎么线上检测和分析

内存泄漏的情况:

表现

怎么避免?

内存泄漏原理

内存泄漏检测原理

怎么检测?

LC原理和步骤

LC不能用于线上的原因

koom

4.2. 弱引用、每次GC弱引用都会被回收吗?no、虚引用、弱引用、软引用的区别

3. 多线程

3.1. 使用方式、状态切换、同步/异步(aync、await)/并发编程、协程、ThreadLocal、LooperThread、安卓主线程与子线程切换handler、锁、DCL、线程池及配置

3.2. 异步,银行家等待资源拿完了又怎么办

3.3. 加锁卡顿

3.4. static加锁有什么问题?

3.5. 调用线程start和run有什么区别?

3.6 线程几种状态和线程状态迁移

3.6 讲一下单例模式

枚举方式呢?

讲一下DCL

synchronized原理

volatile的原理

5. 安卓

5.1. 进程启动流程、资源加载过程、点击launcher图标进行了什么操作?

5.2. 俩activity跳转生命周期

5.3. activity生命周期常见问题

5.3. activity与window的关系,一个activity可以有多个window吗?一个acitvity最少一个window吗

5.4. activity和fragment的区别

5.3. 事件分发原理

5.3.1. 手指触摸屏幕开始会经历什么、从点击屏幕开始到view响应过程、具体分发策略

5.3.2 怎么解决事件冲突

内部拦截法

外部拦截法

NestedScrollTabContainer

CellingNestedScrollTabContainer

ViewPager怎么解决的内部view事件冲突

5.3.5 ACTION_CANCEL在什么情况下会发生

5.4. Handler

5.4.1 消息队列怎么work的

5.4.2. 一个APP只有一个MessageQueue吗?一个线程有几个handler/looper/messageQueue?

handler,新消息来了怎么唤醒去处理的?

threadLocal是啥?

5.4.2. 内存泄漏原因

5.4.3. 解决方法

内存屏障/消息屏障

消息屏障

内存屏障

handler平时遇到的问题

5.5. UI仔必备

自定义view

ui相关

ui问题

 动画

5.6. IPC

5.6.1. 常见IPC方式及aidl底层通信原理

5.6.1. bundle(四大组件)、aidl(代理模式)、binder池、Messenger、文件共享、contentprovider、socket、共享内存

5.6.1. Binder

Binder连接池

选取IPC方式

跨进程通信的缺点?

5.7. 数据库

5.7.1. SQLite

5.7.2. group by和order by的区别

5.8. JNI

使用

NDK内存模式

5.10 四大组件

5.11 平行视界是什么

5.12 软件共版

5.13 项目升级

gradle 4.x->gradle 6.7.x

target35

6. 没听过的

Retrofit(网络)、hook(逆向)

7. 多媒体&音视频

7.1. 音频长短焦点

7.1.1. 音频焦点策略,丢焦点和恢复时机,优先级,混音排查方式

7.2. MediaPlayer

7.2.1. 创建流程、原理

7.2.2. 为什么一个MediaPlayer只能设置一个surface

7.3. ijkplayer

7.4. MediaCodec

7.4.1可以配置的参数

md3信息有哪些

7.5. camera

使用和原理

7.6. ffmpeg

7.6.1. 使用流程调了哪些接口

7.6.2. 使用中有什么缺点

7.7. OpenGL平时拿来干啥

7.7. 图片压缩算法

平时看了什么源码?

8. 设计

8.1. 面向对象编程

8.1.1 描述一下封装、继承、抽象

8.1.2. 优点、缺点、不使用它有什么不好的地方、继承有什么缺点、怎么解决,类膨胀怎么解决

8.1.3. 重写和重载的区别

8.2. MVC/MVVM/MVP

8.2.1. 有什么区别

8.2.2. 只是影响UI刷新吗?hhhhhh

8.2.3. 各自缺点

8.3. 设计模式

8.3.1. 讲一下常用的设计模式和应用场景

8.3.2. 设计原则

8.3.3. 设计有什么用?你平时有用吗?

8.4. 高内聚、低耦合

8.4.1. 为什么要高内聚低耦合?

8.4.2. 什么时候需要解耦?解耦场景

8.4.3. 什么时候该解耦 vs 什么时候不

8.4.4. 解耦度量

8.4.5. 举措

8.5. 其他设计方式

8.5.1. 偏大型互联网app

8.5.2. 偏厂商类型的内置app

8.5.3. 现代化编程

8.5.4. 生产者消费模式

8.5.5. 线程池隔离

8.5.5. 抗弱网

8.5.6. native壳+跨端组件业务

8.6. 设计题

8.6.1 设计一个拖拽容器

8.6.1.1 拖拽的业务场景类型、需求、难点分析

8.6.1.2 容器设计

5.3.7 多点触控

8.6.2 设计一个通用的搜索组件

9. 性能

9.1 平时关注的性能指标有哪些?

9.1.1.  耗时操作有哪些?

9.1.4  卡顿问题排查及原理

9.1.2.  包大小

9.1.3  anr是啥?原理是啥?

10. 跨端

10.1. 公司跨端借鉴的语法和开源的语法的区别,编译生成的产物有什么

11. 技术方案相关

11.1. 怎么在OS桌面上展示一个视频播放小窗

11.2. 保活

11.3. 图片预览时怎么处理大图加载

11.4. 视频录制到一半把进程杀了,怎么拿到已经保存到本地的数据

12. 其他

12.1 怎么做好一个软件,质量标准有哪些

13. 笔试题

14. 项目

14.1. 挑一个参与感重或者有技术难点的项目讲讲,讲一下主要流程

实际没有技术难点可以从以下几个方面入手:

技术卡点

15. git

15.1. 平时用什么git命令?

15.2. git merge和git rebase的区别?

15.3. 如果提交了a b c d四个记录,想改成a c b d咋整?

16. 别人的面试题


1. 数据结构

1.1 常用数据结构有哪些

[]数组、ArrayList列表(可拓容的数组)、

数据结构 类型 日常封装类 底层实现 应用场景
数组 -最基础 基础数据结构 int[]String[] 连续内存 固定长度数据
列表 基础+封装 ArrayList 数组 动态列表(商品列表、用户列表)
链表 -最基础 基础数据结构 LinkedList(不用)、MessageQueue(系统用) 不连续内存 + 指针 Android MessageQueue
基础数据结构 ArrayDequeStack(已经过时)、也可自己用数组或链表实现 数组 页面活动栈、撤销/恢复
队列 基础数据结构 ArrayDeque 数组 消息队列、BFS遍历
双向队列 基础数据结构 ArrayDeque 数组 两端都能操作
哈希表 基础数据结构 HashMap 数组 + 链表/红黑树 映射关系
有序哈希表 基础+扩展 LinkedHashMap HashMap + 双向链表 LRU缓存、映射关系/需要方便移除,查找到值就更新,没找到就插入,同时还能保证先进先出操作顺序。也可以手动维护一个LIFO栈的插入逻辑,即每次新元素都放在map的最前面。重写put和实现getTop。
并发哈希表 基础+扩展 ConcurrentHashMap 数组 + CAS/分段锁 多线程共享映射关系
基础数据结构 自定义(parentId) 数组 省市区联动、多级表单、购物车、ai会话、文件系统目录、多级父类、多个节点且节点嵌套节点、hashmap
写时复制列表 基础+扩展 CopyOnWriteArrayList 数组

读多写少并发场景,比如用户一直读某个数据,数组每几秒更新一次。原理:每次会直接拷贝一个新数组,新数组改完以后再复制给旧数组,改的过程中读到的还是旧数据。

1.2 HashMap、ArrayList、LinkedList、堆、栈、队列

HashMap

HashMap底层是数组+链表+红黑树(jdk1.8以后)。数组默认16,负载因子0.75,扩容阈值是12。当链表长度 ≥ 8 且 数组长度 ≥ 64时,会树化成红黑树来提升查询效率。

核心数据结构:HashMap 内部有一个 Node<K,V>[] table 数组,每个元素是一个“桶”(bucket)。每个 Node 是一个键值对,包含:hash、key、value 和指向下一个节点的 next 指针。如下图所示:

原理:把 key 通过哈希函数,换算成一个数组下标,存到数组的“桶”里。不同 key 算出同一个下标 → 链表 / 红黑树。查的时候:先找桶,再在桶里用 equals 找。这样可以避免hash冲突。

核心流程(put 和 get):put 时,先对 key 做哈希计算,得到数组下标。如果那个位置是空的,直接放进去。如果不空,就遍历链表或树,用 equals 找 key:找到就覆盖,没找到就新增。

拓容机制:数组个数超过16*0.75=12时,数组开始拓容。数组长度翻倍,元素重新计算下标(要么原位,要么移动旧容量)

转红黑树:当链表长度超过 8且数组长度大于等于 64 时,链表查找慢就会转成红黑树,查找从 O(n) 变 O(log n)。如果数组长度小于 64,即使链表长度到 8,也不会树化,而是优先进行扩容。这是为了避免在数组还很小的时候,因为个别桶冲突严重就引入红黑树这种复杂结构,扩容往往就能解决问题。

退回链表:当红黑树的节点数小于等于6时,会退化成链表。

核心设计:哈希函数的扰动——让高位也参与运算,减少冲突;容量固定为2的幂——用位运算替代取模,提升计算下标的性能;负载因子0.75——平衡了时间和空间。它不是线程安全的,所以并发场景要用ConcurrentHashMap。

ConcurrentHashMap

数组+链表+红黑树+CAS+synchronized(jdk1.8以后)

怎么保证线程安全:

写操作:cas或者对每个桶的第一个头节点加 synchronized。

执行 put(key, value)
    │
    ├─→ 计算哈希,找到桶下标 i
    │
    ├─→ 检查 table[i] 桶是否为空?
    │       │
    │       ├─→ 是:用 CAS乐观锁把新节点放进去 → 完成
    │       │
    │       └─→ 否:synchronized (table[i]) {  // 锁住头节点
    │                 // 遍历链表或红黑树
    │                 // 执行插入或更新
    │           }
    │
    └─→ 完成

读操作:无锁(volatile 保证可见性)。

扩容时:多线程一起协助扩容(效率高)。

1.2.1 堆、栈是什么?怎么实现栈?

堆:一种特殊的完全二叉树

栈stack:水桶,先进后出。压栈,页面活动栈。

怎么实现栈?ArrayDequeue、LinkedHashMap。或者使用基础数据结构:数组、链表。

接口 作用
push(item) 压栈:把元素放到栈顶
pop() 出栈:移除栈顶元素并返回
peek() / top() 查看栈顶元素(不移除)
isEmpty() 判断栈是否为空
size() 返回栈中元素个数

数组实现栈思路

  • 用数组存数据

  • 用一个 top 指针指向栈顶位置(通常初始为 -1,表示空栈)

public class ArrayStack {
    private int[] array;
    private int top;      // 栈顶索引
    private int capacity; // 容量
    
    // 构造方法
    public ArrayStack(int capacity) {
        this.capacity = capacity;
        this.array = new int[capacity];
        this.top = -1;  // 空栈
    }
    
    // 1. 压栈
    public void push(int item) {
        if (top == capacity - 1) {
            // 满了就扩容(可选)
            resize();
        }
        array[++top] = item;
    }
    
    // 2. 出栈
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空");
        }
        return array[top--];
    }
    
    // 3. 查看栈顶
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空");
        }
        return array[top];
    }
    
    // 4. 判断是否为空
    public boolean isEmpty() {
        return top == -1;
    }
    
    // 5. 获取大小
    public int size() {
        return top + 1;
    }
    
    // 扩容(加分项)
    private void resize() {
        int newCapacity = capacity * 2;
        int[] newArray = new int[newCapacity];
        for (int i = 0; i <= top; i++) {
            newArray[i] = array[i];
        }
        array = newArray;
        capacity = newCapacity;
    }
}

链表实现栈

  • 用单链表,头插法:每次插入都在头部

  • 头部就是栈顶

public class LinkedListStack {
    
    // 节点内部类
    private static class Node {
        int value;
        Node next;
        Node(int value) {
            this.value = value;
        }
    }
    
    private Node head;  // 栈顶(链表头)
    private int count;  // 元素个数
    
    // 1. 压栈:头插法
    public void push(int item) {
        Node newNode = new Node(item);
        newNode.next = head;
        head = newNode;
        count++;
    }
    
    // 2. 出栈:删除头节点
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空");
        }
        int value = head.value;
        head = head.next;
        count--;
        return value;
    }
    
    // 3. 查看栈顶
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空");
        }
        return head.value;
    }
    
    // 4. 判断是否为空
    public boolean isEmpty() {
        return head == null;
    }
    
    // 5. 获取大小
    public int size() {
        return count;
    }
}
1.2.2 数组和列表的区别

数组[]

int[] arr = new int[5];

列表是可自动拓容的数组。

ArrayList<Integer> list = new ArrayList<>();

1.2.3 ArrayList与LinkedList的区别、选择
  • ArrayList是动态数组,头部插入慢,访问通过索引访问非常快,仅存储数据和数组容量因此内存占用小

  • LinkedList是双向链表,头部插入快,访问需要从头到尾遍历慢,每个节点需要存储前后节点的指针因此内存开销较大

一般是用ArrayList,除了一些特殊常见外

  • 频繁按索引随机访问元素(读多写少),用 ArrayList

  • 需要遍历列表元素,需要频繁在列表头部或中间进行插入和删除操作(写操作多),需要实现栈(Stack)、队列(Queue)或双端队列(Deque),不确定操作类型,但不会进行大量的随机访问,选LinkedList

1.2.4 ArrayList原理

拓容

2. 语言

2.1. JAVA

2.1.1. JVM

2.1.2. 介绍一下双亲委托机制,对其有什么理解、java内存模型、gc生命周期

双亲委托机制

当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成。父类处理不了才交给子类处理,保证了类不会重复加载,核心API不会被改变(因为最终会走到父类去加载),每个加载器都有明确的加载范围,形成了清晰的层次结构。跟安卓事件分发有点点像,都用到了责任链模式。

双亲委托可以:

1.防止核心API被篡改

2.避免恶意代码注入

3.解决类、版本冲突的关键机制

4.热修复,通常会“破坏”或“绕过”双亲委托来实现功能。

5.分析classLoader内存泄漏、分析classNotFoundException的原因

这里一般会往热修复、hook、插件原理、逆向上扯,呀咩喋,学不完根本学不完

此处关联一个热修复框架tinker。

tinker

原理:差分->合并->插队。

服务端使用DexDiff算法获取新旧代码apk的补丁包,客户端在本地把补丁patch.dex和原dex合并成完整文件,再通过反射把它插入到系统类加载器数组的最前面,实现修复代码的优先加载。

内存模型
  • 虚拟机栈:

局部变量表(存储成员变量和方法传参里面的参数)

操作栈(存储要进行操作的变量,进行算数操作)

动态链接(获取类或者接口并将其组合到java虚拟机的运行时状态以便可以执行的过程)

返回地址(记录调用方法的地方,等方法执行结束以后要回到这个地址继续执行)

  • 本地方法栈:JNI调用的方法

  • 方法区:存储类模板、常量、静态变量

  • 堆:存储实例对象

  • 程序计数器:记录程序运行到了那一步,方便线程切换回来时继续上一次操作

内存分布主要是线程共享区和线程独占区。

线程共享区:方法区和堆区。而事实上堆和方法区都是在内存(物理内存)中

线程独占区:栈、程序计数器。栈则是在高速缓冲区里,高速缓冲区是手机CPU里的“内存”,可以理解为它相当于CPU的“口袋”,所以这也让它的计算速度比在内存里要快(离CPU近)

代码在内存里的呈现

GC生命周期

GC(垃圾回收)的生命周期是指从对象创建到被回收的整个过程,主要分为以下几个阶段:

对象创建与内存分配  -》可达性分析  -》垃圾标记  -》回收  -》内存整理  -》内存释放

1.对象创建与内存分配

对象在堆内存中被创建(例如new Object())

内存分配方式取决于垃圾回收器:

  • 新生代(Young Generation):大多数对象首先分配在新生代的 Eden 区。

  • 大对象:可能直接进入 老年代(Tenured Generation)或 巨型对象区域(取决于 JVM 实现)。

2.可达性分析

  • GC 通过GC Roots(如栈帧中的局部变量、静态变量、JNI 引用等)作为起点,遍历对象引用链。

  • 可达对象:能被GC Roots 直接或间接引用的对象。

  • 不可达对象:没有任何引用链连接到GC Roots,被标记为垃圾。

3. 垃圾标记

  • 标记阶段:遍历堆中对象,标记哪些是“存活”的,哪些是“垃圾”。

  • 常见的标记算法:

    • 标记-清除(Mark-Sweep)

    • 并发标记(如 CMS、G1 的初始标记、并发标记阶段)

4. 回收阶段

根据不同的 GC 算法,回收方式不同:

新生代回收(Minor GC)

  • 复制算法:将 Eden 区和 Survivor From 区存活的对象复制到 Survivor To 区,年龄+1,然后清空 Eden 和 From 区。

  • 对象年龄达到阈值(默认 15)后,晋升到 老年代。

老年代回收(Major GC / Full GC)

  • 标记-清除 或 标记-整理算法:清除不可达对象,整理内存碎片。

  • 触发条件:老年代空间不足、显式调用 System.gc()、空间分配担保失败等。

5. 内存整理

  • 标记-整理算法:将存活对象向内存一端移动,消除碎片,便于后续分配。

  • 典型GC 器:Serial Old、Parallel Old、G1(部分区域整理)。

6. 内存释放

  • 回收的内存被归还到JVM 内存池,供后续对象分配使用。

  • 某些 GC(如 ZGC、Shenandoah)几乎全程并发,停顿时间极短。

 

  • 新生代

占据堆空间的1/3,又细分为三个区:Eden、SurvivorFrom、ServivorTo,三个区的默认比例为:8:1:1。新生代保存着大量的刚刚创建的对象,而新生代中会频繁进行垃圾回收。可使用标记-复制法。

  • 老年代

占据堆空间的2/3,主要存放应用中生命周期长的对象,老年代不会频繁进行垃圾回收。可使用标记清除法、标记整理算法

垃圾回收算法

1、标记-清除法:

标记所有可以回收的对象,然后统一进行回收。

缺点:会产生很多不连续的内存碎片,效率不高。

2、标记-复制法:将内存区域划分为两块大小一致的区域(比如A和B),每次只使用其中一块(比如A),当A块的内存用完了之后就将还存活的对象复制到B区域上去,然后将当前块A的内存空间一次清理掉。

缺点:内存利用率永远只有一半、浪费太大,存活多时复制成本高、消耗性能。

3、标记-整理法:先标记所有可以回收的对象,然后将存活的对象向内存空间的一端进行移动,将所有存活的对象都连续的放在一起,然后直接清理掉边界以外的内存。

缺点:性能开销大。

问到这里一般会往内存分析、资源分配、线程、锁上扯, 呀咩喋,学不完根本学不完。内存分析跟性能挂钩比较大,性能会影响最终程序表现。

2.2.1 异常处理机制Throwable 

Throwable 是 Java 异常体系的根父类,它有两个直接子类:Error和Exception

类别 说明 常见例子
Error 系统级严重错误,程序不应该捕获处理 OutOfMemoryErrorStackOverflowErrorNoClassDefFoundError
Exception 程序运行中可处理的异常 IOExceptionSQLExceptionNullPointerException

Exception又分为两类:runtimeexception和非runtimeexception

类型 是否必须处理 常见例子
RuntimeException(非受检) 不强制,编译器不检查 NullPointerExceptionArrayIndexOutOfBoundsExceptionArithmeticException
非RuntimeException(受检) 必须处理(try-catch 或 throws) IOExceptionSQLExceptionClassNotFoundException

长这样

Throwable
├── Error(系统错误,不该捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── VirtualMachineError

└── Exception(程序可处理)
    ├── RuntimeException(非受检,不强制处理)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ArithmeticException
    │   └── IllegalArgumentException
    │
    └── IOException(受检,必须处理)
        ├── FileNotFoundException
        ├── EOFException
        └── SQLException

异常处理的关键字

关键字 作用
try 包裹可能出异常的代码
catch 捕获并处理异常
finally 无论是否异常都会执行(关闭资源等)
throw 手动抛出异常对象
throws 声明方法可能抛出异常

2.2. Kotlin

2.2.1. 怎么理解空安全检测,安全检测一定能避免这些问题吗?

空安全检测

java常见问题:NullPointerExcetion。kt空检测在编译时期就大概率的避免这一问题的出现,代码简洁

使用方式

  1. 不能把null赋值给其他类型的基础数据或者对象的成员变量、方法变量、类变量

  2. 函数式、链式编程安全调用,对象?.

安全检测一定能避免NPE吗?

能大部分时候避免NPE,但是代码使用一些奇技淫巧可能还是会NPE,下面的操作也可能会NPE

  1. 使用!!判断这里不为空,实际运行的时候为空了

  2. 与java互操作时,java返回空但是没被识别出来

  3. lateinit未初始化就使用

  4. 数组中的类型检测绕过编译,但运行的时候用到这个数组中的某个对象的属性

  5. 反射、JNI

4. 内存

4.1. 内存泄漏

4.1.1. 表现、原理、怎么避免、怎么线上检测和分析
内存泄漏的情况:
  1. 单例模式。

  2. 静态变量持有的引用导致的内存泄露。

  3. 非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象生命周期长时,就会内存泄露,比如常见的Handler,Thread,AsyncTask,创建一个线程去作网络请求。

  4. 未取消注册或回调导致的内存泄漏。

  5. 集合中的对象未清理造成内存泄漏。

  6. 资源未关闭或未释放导致内存泄漏,比如流对象、流式RPC、WebView、地图等使用完后要及时关闭。

  7. Handler造成的内存泄漏

  8. 线程造成的内存泄漏

  9. 地图、视频播放器、图片加载、webview、流式数据停止使用或者退出页面的时候没有反注册。

  10. 也不一定是页面,如果一个列表每个item都去注册一次webview,但是item不可见或者销毁的时候没有反注册,也可能会导致后面的数据渲染不出来。

表现
  1. 程序异常(case by case)

  2. 程序越使用内存占用越大且内存占用增长较快

  3. 频繁GC但是内存得不到有效的释放

  4. 因为频繁的GC和生成对象和内存碎片化界面响应越来越慢

  5. 界面渲染卡顿、滑动卡顿或者view组件渲染不出来又不报错、白屏

  6. 最终OOM闪退

怎么避免?
  1. 记得反注册

  2. 合理使用缓冲策略

  3. 使用weakrefrence

  4. 及时释放资源,如流式数据、webview

  5. 避免静态集合无限制增长

内存泄漏原理

创建一个对象,也就是在堆区里申请多一块空间给它,如果某个对象一直持有这块空间,该空间无法得到释放。所以如果内存泄露的次数多,最终很容易引起内存溢出。

  • 程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。

  • 对象在引用链上,但是已经不可用了。根可达,但是内存已经不能再用了。

内存泄漏检测原理

jvm判断对象应该被回收的方式:

  • 引用计数法

当对象没有其他对象在引用该对象时,应该被回收。

  • 可达性分析

GC roots(静态变量、线程栈变量、常量池、JNI指针)

在GC roots的引用链上可以找到对象的时候,就认为这个对象根可达,GC来回收时发现该对象根可达,该对象就不应该被回收。在引用链找不到该对象,该对象不可达,就应该被回收。

怎么检测?

线下MAT,俩Ativity跳来跳去,比较heap堆栈,过滤掉弱引用虚引用啥的就是泄漏的方法

线上检测LeakCanary,主要是hook页面生命周期,hook生命周期这种操作也可以用来检测白屏。

LC原理和步骤

原理:可达性分析。根可达但无用。利用 WeakReference + ReferenceQueue 来检测对象是否应被回收。

步骤:

  1. 监测Activity 的生命周期的 onDestroy() 的调用。

  2. 当某个 Activity 的 onDestroy() 调用后,便对这个 activity 创建一个带 ReferenceQueue 的弱引用,并且给这个弱引用创建了一个 key 保存在 Set集合 中。

  3. 如果这个 activity 可以被回收,那么弱引用就会被添加到 ReferenceQueue 中。

  4. 等待主线程进入 idle(即空闲)后,通过一次遍历,在 ReferenceQueue 中的弱引用所对应的 key 将从 retainedKeys 中移除,说明其没有内存泄漏。

  5. 如果 activity 没有被回收,先强制进行一次 gc,再来检查,如果 key 还存在 retainedKeys 中,说明 activity 不可回收,同时也说明了出现了内存泄漏。

  6. 发生内存泄露之后,dump内存快照,分析 hprof 文件,找到泄露路径(使用 haha 库分析)

LC不能用于线上的原因

LeakCanary 会主动GC、Dump内存快照,造成卡顿和ANR,只适合开发阶段,不能用于线上。

  1. 频繁gc造成掉帧卡顿

  2. dump内存快照耗时会造成anr

  3. hprof上传文件太大,耗时耗流

  4. 重复dump,手机内存爆满

koom

检测思路跟LC差不多,线上可以使用这个监控内存泄漏。

不同的是触发方式、gc时机不同、分析方式,还要可以检测native泄漏和thread泄漏。

koom不主动 GC,不阻塞主进程。内存占用率 >= 80% && 连续检测超过3次时,fork一个子进程去异步分析dump文件,卡也只卡子进程,且本地分析以后只把分析结果上传到服务器。

4.2. 弱引用、每次GC弱引用都会被回收吗?no、虚引用、弱引用、软引用的区别

引用类型 GC时机 典型用途 是否影响GC 引用队列
强引用 从不回收 普通对象引用 ❌ 不影响 ❌ 无
软引用 内存不足时回收 缓存 ✅ 内存敏感 ✅ 可选
弱引用 下次GC就回收 缓存、监听器 ✅ 立即回收 ✅ 可选
虚引用 随时可能回收 对象回收跟踪 ✅ 跟踪回收 ✅ 必须

引用类型及回收时机如上,弱引用不会马上被回收,只有对象被弱引用指向时才会在GC时被回收,不会立即回收,是在下次GC扫描到该对象时被回收。

public class WeakReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object();
        WeakReference<Object> weakRef = new WeakReference<>(obj);
        System.out.println("GC前: " + weakRef.get()); // 有值
        // 关键:强引用还在!
        // obj 这个强引用仍然指向对象
        // 所以GC不会回收
        System.gc();
        System.out.println("第一次GC后: " + weakRef.get()); // 还有值!
        // 只有当强引用断开时
        obj = null;  // 断开强引用
        System.gc();
        System.out.println("第二次GC后: " + weakRef.get()); // null
    }
}

使用场景如下:

场景 推荐引用类型 理由
图片缓存 软引用(LruCache) 内存敏感,但可重新加载
临时数据缓存 弱引用(WeakHashMap) 可自动清理,避免泄漏
监听器集合 弱引用 防止忘记移除导致泄漏
堆外内存 虚引用 + 引用队列 精准释放,避免内存泄漏
必须可用的数据 强引用 + LRU策略 确保可用性,自己控制生命周期
对象回收跟踪 虚引用 比finalize()更可靠

3. 多线程

3.1. 使用方式、状态切换、同步/异步(aync、await)/并发编程、协程、ThreadLocal、LooperThread、安卓主线程与子线程切换handler、锁、DCL、线程池及配置

创建线程的方式

继承Thread、实现Runnable接口、使用线程池

安卓线程中的有哪些

AsyncTask(下载任务,已过时)、HandlerThread、IntentService、ThreadPoolExecutor(FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor)

线程池可配置参数

核心线程数、优先级、所能容纳的最大线程数、非核心线程闲置时的超时时长、线程池中的任务队列、线程工程

3.2. 异步,银行家等待资源拿完了又怎么办

任务发起后,不必等待其完成,可以立即返回去做别的事情。任务完成后,通过回调、Future/Promise 等机制通知发起者。

3.3. 加锁卡顿

高性能并发编程,一些性能要求高的地方直接加锁比如synchronized、volatile性能损耗较大,可能造成View卡顿。

原因

  • 上下文切换:线程阻塞/唤醒需要内核参与

  • 缓存失效:CPU缓存行失效,需要重新加载

  • 内核态切换:用户态→内核态的切换开销

  • 锁竞争:线程排队等待,吞吐量下降

  • 优先级反转:高优先级线程等待低优先级线程

解决方法

CAS、Atomic相关类(其底层也是CAS)、ThreadLocal、Concurrent相关类(分段segment+CAS)、CopyOnWrite相关类、读写锁、乐观锁

3.4. static加锁有什么问题?

方法

普通方法加锁,比如加个synchronized是锁该方法所在类的类实例对象,多个线程访问同一个对象的该方法会互斥,不同线程访问不同对象的该方法不会互斥,锁的粒度是实例级别。

static方法加锁是锁类的class 对象多个线程访问该类的任何实例的这个方法都会互斥,因为所有实例共享同一个class 对象,锁的粒度是类级别。

变量

多线程操作static的变量会有严重的线程安全问题。

非原子操作,一个线程修改后另外一个线程不可见,指令重排序。

解决方式

使用synchronized+volatile、AtomicInteger、ThreadLocal等锁

3.5. 调用线程start和run有什么区别?

开启一个子线程需要调用start,start会调到run方法。如果只调run方法,线程则还是在调用线程执行。

对比维度 start() 方法 run() 方法
线程创建 创建新线程,并行执行 不创建新线程,在当前线程同步执行
调用次数 只能调用一次,多次调用抛异常 可以多次调用
执行方式 异步执行 同步执行
线程状态 使线程进入就绪状态,等待CPU调度 只是普通方法调用,不改变线程状态
JVM控制 JVM调用线程的 run() 方法 直接执行 run() 方法体
3.6 线程几种状态和线程状态迁移
  1. NEW创建

  2. RUNNABLE(RUNNING/READY)运行

  3. WAITING等待

  4. TIMED_WAITING超时等待

  5. BLOCKED阻塞

  6. TERMINATED终止

  • new一个thread到NEW状态,thread.start到RUNNABLE状态

  • object.wait()/object.join()就到WAITING状态,thread.spleep(n)/object.wait(n)/object.join(n)到TIMED_WAING状态,他们可以通过object.notify()/object.notifyall()唤醒回到RUNNABLE

  • 如果RUNNABLE过程中syncronized方法/对象等待锁,就到BLOCKED,等待到了锁就回到RUNNABLE

  • 线程执行完成/终止就到TERMINATED

3.6 讲一下单例模式
枚举方式呢?

讲一下DCL

synchronized原理

volatile的原理

5. 安卓

5.1. 进程启动流程、资源加载过程、点击launcher图标进行了什么操作?

点击Launcher -> AMS响应检查进程是否存在?->不存在Zygote就fork出一个新应用进程->ActivityThread.main->新进程attach到AMS

->ApplicationThread回调进程->Application的oncreate->通过androidmanifest.xml找到第一个activity创建activity对象

->创建ContextImpl并加载应用资源Resources和AssetManager->调用Activity.attach(),将ContextImpl、Application、Window等关键对象赋给Activity

->Activity的oncreate/onstart/onresume。oncreate中通过setcontentview设置布局

->ViewRootImpl->执行performTraversals()->触发测量(Measure)、布局(Layout)、绘制(Draw)流程->通过SurfaceFlinger将像素数据合成并显示到屏幕上

5.2. 俩activity跳转生命周期

当 Activity A 启动 Activity B 时,它们之间会有一系列生命周期的交替调用,这个顺序的设计主要是为了保证用户体验的流畅,即尽快地把新的界面展示出来。

假设 Activity A 当前处于前台可见、可交互的状态(即 onResume 状态),此时我们调用 startActivity 来启动 Activity B。生命周期的调用顺序如下:

 A onPause() -> B onCreate() -> B onStart() -> B onResume() -> A onStop()

这个顺序的关键点在于 A 的 onStop() 是在 B 的 onResume() 之后才调用的。这样设计的核心目的是为了让新启动的 Activity B 能够尽快地显示出来,减少用户等待的白屏时间。系统会等到 B 完全准备好并显示给用户后,才去处理已经不可见的 A 的停止逻辑。

具体原因如下:

A: onPause()

系统首先会调用 Activity A 的 onPause() 方法。这标志着 A 即将失去焦点,不再与用户直接交互。在这个回调里,我们应该做一些轻量级的操作,比如停止动画、释放摄像头等资源,或者保存一些未提交的持久化数据。这个方法必须非常快地执行完毕,因为它会阻塞 Activity B 的启动流程。
B: onCreate()

在 A 的 onPause() 执行完之后,系统会立即开始创建 Activity B 的实例,并调用其 onCreate() 方法。这是 B 生命周期的开始,我们在这里进行所有的初始化操作,比如 setContentView() 加载布局、初始化变量、绑定 ViewModel 等。
B: onStart()

onCreate() 执行完毕后,Activity B 的 onStart() 方法会被调用。这表示 B 的窗口已经创建,并且即将变得可见。
B: onResume()

紧接着,Activity B 的 onResume() 方法被调用。此时,B 已经完全可见,并获取了用户焦点,处于前台可交互状态。至此,新界面已经完整地呈现给用户了。
A: onStop()在 Activity B 的 onResume() 执行完毕,并且 B 的窗口已经完全覆盖了 A 的窗口之后,系统才会调用 Activity A 的 onStop() 方法。这表示 A 已经完全不可见了。如果 A 在 onPause() 中没有完成所有的停止工作,这里是进行更重量级停止操作的最后机会。

这里还有一个特殊情况需要注意:如果 Activity B 是一个透明主题(Translucent)或者是一个 Dialog 样式的 Activity,那么它不会完全覆盖 Activity A。在这种情况下,Activity A 将只会调用 onPause(),而不会调用 onStop(),因为它依然是部分可见的。

如果配置了活动栈类型又不太一样。

5.3. activity生命周期常见问题

  • 有的弹窗使用透明acitvity,关闭后下面的acitivty会onresume,如果这个activity的onresume有做操作,会触发这个方法导致页面数据拉取刷新啥的

  • 为了页面启动快,有的公司的页面跳转用了堆栈的方式,即分别跳转ABABABA的活动页,则它们会依次进栈,虽然只有2个类型的页面,但活动栈里面7个页面实例,有如果页面太多可能会占用内存太多导致闪退。

  • 有的页面有轮询操作,它会配置singletop然后startAvitivty自己,这样每次不会重新创建一个活动页,该活动页的成员变量得以保存留给下一个轮询操作使用。启动的时候生命周期是:onNewIntent() → onRestart()→ onStart()→ onResume()

  • activity跳转会白屏,推荐使用fragment进行切换

  • 开发过程中除了acitivity有生命周期,还比较经常用到的是其他技术栈(鸿蒙、跨端)的页面、组件生命周期,包括自己写容器提供给其他业务或者作为跨端组件容器也需要自己mock提供一个生命周期。常见组件对比如下:

创建 可见 可交互 不可交互 不可见 销毁 返回 滑动中 滑动停止

acitvity
一个可点击/滑动出来的抽屉
一个跨端组件容器

5.3. activity与window的关系,一个activity可以有多个window吗?一个acitvity最少一个window吗

window是activity的ui容器。可以通过windowmanager来改变view层级啥的。

一个Activity最少有一个Window(主Window),一个Activity可以有多个Window,但只有一个主Window。比如可以弹出多个dialog,一个dialog一个window。

5.4. activity和fragment的区别

activity是页面级别,是fragment的容器

如无特殊情况,对一些性能要求高的地方建议用碎片,可以避免页面切换时的空白。

5.3. 事件分发原理

5.3.1. 手指触摸屏幕开始会经历什么、从点击屏幕开始到view响应过程、具体分发策略
  • 传递过程:物理触摸 -> IMS(InputManagerService) -> WMS(WindowManagerService) -> App Process -> ViewRootImpl -> DecorView -> Activity,然后触摸事件开始分发。

  • 具体分发策略:

1.由Activity的dispatchTouchEvent向下分发。事件到达ViewGroup后,会先调用onInterceptTouchEvent判断是否拦截。如果拦截就到onTouchEvent,如果不拦截,就继续分发给子View。

2.子View的dispatchTouchEvent会调用onTouchEvent来处理事件。如果某一层的onTouchEvent返回true或者设置了click事件就消费了事件,传递就终止。如果这个view为viewgroup则会有个intercept判断,没有拦截再继续往下下发。如果所有子View都不消费,事件会逐级回传给父容器和Activity的onTouchEvent。

3.并且,一旦某个View消费了ACTION_DOWN,后续的MOVE和UP事件会直接分发给它。核心3个方法:dispatchTouchEvent(事件分发)、onInterceptTouchEvent(事件拦截)onTouchEvent(事件处理),true表示事件消费或者拦截,false表示否

其他拦截场景:比如horizaly、requestparentdisallow等非常规场景,

常见组件:recyclerview、nestedscrollview、viewpager1、viewpager2

与之关联的还有onClick、Gesture、手势

5.3.2 怎么解决事件冲突
内部拦截法

外横内竖直,父容器不拦截任何事件,父布局也要改一下其onInterceptTouchEvent里面的拦截方法,把down放开。

重写子元素的dispatchTouchEvent方法,搭配requestDisallowInterceptTouchEvent处理。

//父容器拦截ACTION_MOVE,在子容器里面调这个

mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);

外部拦截法

外横内竖,重写父容器的onInterceptTouchEvent。横向距离>竖向滑动距离就外部拦截

NestedScrollTabContainer

竖直方向冲突外部一个scrollview里面一个rv。

问题:headview展示中,rv上滑时,headview没有滑上去,因为被子view消费了。

安卓是重写nestedscrollview.onNestedPreScroll,如果header没有隐藏就把滑动距离通知给外层让它可以滑上去。内部rv再消费

抽象成鸿蒙就是:竖直方向滑动冲突,外面一个rv,里面一个rv,里面滑需要带动外面滑,外面滑动需要带动里面的滑。需要解决的问题

- 手指上滑(scrollForward):外层 Scroll 先消费(滚走 Header),然后内层 List 再滚 => PARENT_FIRST

- 手指下滑(scrollBackward):内层 List 先消费(滚到顶),然后外层 Scroll 再滚(显示 Header) => SELF_FIRST

CellingNestedScrollTabContainer

tabindicator需要吸顶

让tablayout.height+viewpager.height=nestedScrollView.height,等内部往上滑tablayout滑到顶部的时候就滑不动了,固定住了,内部rv再继续滑动。,从而达到顶部tabindicator吸顶的效果

ViewPager怎么解决的内部view事件冲突

5.3.5 ACTION_CANCEL在什么情况下会发生

ACTION_CANCEL:

当父视图的 onInterceptTouchEvent() 方法从返回 false 变为返回 true 时,子视图会收到 ACTION_CANCEL

触摸边界超出控件范围、系统事件打断(压后台/锁屏/来电/弹出系统弹窗/)、窗口失去焦点、滚动容器开始滚动

5.4. Handler

5.4.1 消息队列怎么work的

图. handler工作流程

工作流程:通过handler.sendmessage/postmessage发送消息,线程调用looper.loop循环处理messagequeue的队列消息,如果消息队列为空或者未到处理时间戳则进行阻塞等待新消息。

1. Handler 发送消息 → 按时间排序插入 MessageQueue
2. Looper.loop 无限循环调用 queue.next()
3. queue.next():
   - 有消息且已到时间 → 立即返回消息
   - 有消息但未到时间 → 阻塞到执行时间
   - 无消息 → 永久阻塞等待新消息
4. Looper 获取消息后派发给对应 Handler 处理
5. 只有调用 quit() 时才会真正结束循环(主线程 Looper 不能 quit)

子线程:hander.sendMessage-》messageQueue.enqueueMessage消息入队列以后通过在主线程无限循环的looper来获取消息处理消息。

主线程:main创建的时候会创建一个looper.loop->MessageQueue.next->handler.dispatchMessage->handler.handleMessage

Handler:消息的发送和接收。post/send、handmessage

Looper:消息循环动力,不停的从MessageQueue中查看是否有新消息,有新消息就处理,否则一直等待。

MessageQueue:插入/读取消息。handler的MessageQueue是一个按Message.when排序的单项链表。通过 nativePollOnce 和 nativeWake实现延迟和唤醒机制。它设计简单高效,专门为 Android 单线程模型中的轻量级任务调度而优化。

Message:消息。里面常用的有what、arg1数据、arg2数据、obj对象。可以通过handler.obtainMessage来获取消息,好处就是它是从消息池里面拿的,没有就创建一个,有就直接用,减少消息创建消耗。

5.4.2. 一个APP只有一个MessageQueue吗?一个线程有几个handler/looper/messageQueue?

主线程默认只有一个MessageQueue,其他子线程如果创建了也有可能又多个。

一个线程可以有多个handler,new几个有几个。

一个线程只有一个looper和messageQueue,looper在prepare的时候实例化出来,通过threadlocal  map(key,value)的方式来绑定当前线程和looper,消息处理时根据msg.target来判断消息是从哪个hander来的,把消息回调给对应的handler

handler,新消息来了怎么唤醒去处理的?

looper.loop提供动力,无限循环调用 queue.next()从messageQueue中查看是否有新消息,,没有新消息就永久等待,等新消息来了就通过nativeWake唤醒。

threadLocal是啥?

线程副本。是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的地方。每个线程有自己的线程副本,多线程之间访问的是自己线程的值,互不干扰。

实例:handler、ActivityThread、AMS。

handler使用threadlocal的原因:ThreadLocal 避免了多线程共享 Looper 时,为了维护线程→Looper 映射关系(获取当前线程的loooper)而引入的并发控制开销,通过空间换时间,让每个线程独立持有 Looper。

意思就是多个线程都可以new looper,有了threadlocal以后,就是自己线程拿自己线程的looper了。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();
5.4.2. 内存泄漏原因

enqueueMessage的时候会msg.target = this,因此msg持有handler

sendMessage的时候会把msg传给messageQueue,因此messageQueue持有msg

messageQueue由mLoop.mQueue持有,因此looper持有messageQueue

mThreadLocal.set(new Looper(quitAllowed)),因此looper被Looper里面的ThreadLocal静态变量持有

static ThreadLocal 是GC Root(静态变量、常量),被gc root直接或者间接引用的都不能被垃圾回收,因此导致内存泄漏

acticity <- handler <- msg <- messageQueue <- looper <- static threadLocal

5.4.3. 解决方法
  1. 静态内部类+弱引用:static Handler handler的时候可以解决改问题。static修饰的匿名内部类不会持有外部对象

  2. 退出activity的时候移除掉所有消息,mQueue.removeMessages(this, r, null);

  3. viewmodel+livedata

内存屏障/消息屏障
术语 所属领域 作用
内存屏障(Memory Barrier) CPU/编译器指令 防止指令重排序,保证内存可见性(Java volatile底层
消息屏障(Message Barrier) Android Handler机制 拦截普通消息,优先执行异步消息(UI绘制优化
消息屏障

消息屏障是一个targetnull的Message,插入MessageQueue后,Looper在取消息时会跳过所有普通同步消息,只执行异步消息,用于保证UI绘制任务优先执行。

其本质是让UI绘制任务插队,保证doFrameVSYNC信号到来时能立即执行,避免被业务消息阻塞导致掉帧。

// ViewRootImpl.requestLayout() 时
void scheduleTraversals() {
    // 1. 插入消息屏障(隐藏API)
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    // 2. 发送异步消息(执行doFrame)
    mChoreographer.postCallback(..., mTraversalRunnable, ...);
}

内存屏障

Java并发编程里的概念,和volatilesynchronized底层实现相关。

内存屏障是CPU或编译器插入的一道指令,用于防止指令重排序,并强制刷新缓存到主内存,保证多线程环境下的内存可见性。

volatile的底层就是通过内存屏障实现:写操作前插入StoreStore保证之前的写都刷到内存,写操作后插入StoreLoad保证其他线程能立即看到;读操作前后插入LoadLoadLoadStore,保证读到的数据是最新的。

没有内存屏障(CPU可能重排序):
代码顺序:a=1; b=2; flag=true;
实际执行:flag=true; b=2; a=1;  ← 被重排了!

有内存屏障:
在flag=true前插入StoreStore屏障 → 保证a和b先写入
在flag=true后插入StoreLoad屏障 → 保证其他线程立即可见
屏障类型 作用
LoadLoad 禁止读-读重排序
StoreStore 禁止写-写重排序
LoadStore 禁止读-写重排序
StoreLoad 禁止写-读重排序(最重,开销最大)
handler平时遇到的问题

handler.post,会将消息插入队列。如果有人想不让ui卡顿就会使用handler.post的方式去将任务插到消息队列里面排序。

会出现的问题:流程1-》2-》3,但是第2步用了handler.post。当冷启动/任务多/性能不好的时候2就会在前面其他任务处理完才执行,被延迟一点执行,导致流程变成了1-》3-》2。

之前遇到过2次这个问题,一次编排生命周期的时候极端情况下生命周期在onstop之后才给别人回调onstart导致白屏检测异常。

一次是流式渲染的时候,批量任务过来导致步骤跟代码写的步骤跑起来不一样,出了几个bug。

post之后经常持有acitivty的view之类的,这个需要用弱引用

5.5. UI仔必备

自定义view

继承自View或ViewGroup(...Layout)、style、attr、三个构造函数

onMeasure(测量)、onLayout(布局)、OnDraw(绘制)

match_parent、wrap_content

基础(找到view、坐标获取、位移、透明度、缩放、渐变)、动画(属性动画/帧动画/补位动画/转场动画/路径动画/粒子动画)->创建/播放/取消/暂停/继续播放/打断/重复/销毁、插值器、弹性事件、贝塞尔曲线、canvas、事件拦截与分发(热区/长按/快速点击/触摸/fling/加速度/跟手交互gesture、拖拽、事件冲突)、弹窗、气泡、view层级、屏幕宽高、bitmap、ImageView、window、surface、textview、textureview、onConfigChange、动态计算、首帧(ongLobalLayout)

ui相关

除了使用已有组件或者在已有组件上拓展或者使用自定义view的方式自己造轮子,ui展示这一块还有很多东西。如:高斯模糊、分屏(同一个window画面分屏/不同window展示同一个画面/同一个屏幕展示多个window且window画面不一样/不同屏幕展示同一个画面)、lottie、3D、分块/栅格化、数据可视化、透明视频、根据已有的view进行挖空填充一些交叉技术栈或者样式

偏展示的自定义view(如歌词,需要根据解析到的lrc文件来展示信息,如果需要根据声音来展示高亮需要框架返回源数据,用户可以拖拽该view定位并播放抬手时最近的一段歌词对应的音频)

偏交互的自定义view(如自定义自适应文本indicator)、拖拽删除补位动画、常见的列表聚散动画一般每个item的动画样式不一样但是有固定的规律且需要好几个不同动画效果的帧衔接在一起

折叠屏/大屏/小屏/宽屏/窄屏/超低端机适配、、其他安卓不支持或者性能太差的需要三方库支持,比如高斯模糊和分屏可以用openGL、lottie和3D需要引擎支持,其他都是根据业务来,一般需要与业务联动,根据业务规则和用户行为做不同的静态或者动画效果展示

交叉技术栈:偏营销的业务经常用到,如native与h5交互、native与一些跨端交互,需要事件能够相互透传,ui组件生命周期正常。高性能和炫酷画面效果的技术栈会有用到NDK/JNI、Unity 3D、unreal等。

其他:多语言、RTL/LTR、全局皮肤/token/暗黑白天模式、view和window抑制与管控(常用于根据业务规则可能同一时间展示多个view导致view叠在一起的比较大的页面)、字体(样式、大小处理缩略/跑马灯)

ui问题

白屏/黑屏/绿屏/花屏、加载页面/数据卡顿、滑动不流畅、点击事件迟响应或者不响应、防快速点击、ui错位、资源加载失败、列表数据部分不渲染、页面跳转闪烁、页面或者组件生命周期异常导致的业务异常、屏幕发生变化的时候比如翻转折叠展开锁屏压后台以后页面的变化宽高可能拿不准。

 动画

帧动画、补间动画、属性动画。补位动画/转场动画/路径动画/粒子动画

5.6. IPC

5.6.1. 常见IPC方式及aidl底层通信原理
5.6.1. bundle(四大组件)、aidl(代理模式)、binder池、Messenger、文件共享、contentprovider、socket、共享内存

安卓基础知识学习_IPC

aidl底层通信原理:bindler池

5.6.1. Binder

Binder是 Android的跨进程通信(IPC)机制,基于C/S架构,核心是通过mmap实现一次数据拷贝,性能远高于传统的 Socket 和管道。

Binder的工作机制图如下图所示。

图 Binder的工作机制

binder工作机制(从 client挂起到server执行再唤醒client的完整细节,一次具体调用的内部流程)

  1. Client 挂起:客户端通过transact() 把参数写入data,然后线程挂起等待结果。

  2. 驱动转发:Binder驱动将请求从客户端进程拷贝到服务端进程。

  3. 服务端执行:服务端线程池里的线程调用onTransact() 解析参数、执行业务逻辑,结果写入 reply。

  4. 返回并唤醒:驱动把reply传回客户端,挂起的客户端线程被唤醒,拿到结果继续执行。

binder工作流程(完整的跨进程调用链路,偏整体架构与使用流程)

  1. 注册服务:Server向ServiceManager注册自己的Binder引用

  2. 获取服务:Client向ServiceManager请求Server的Binder引用

  3. 发起请求:Client 通过Binder引用调用方法,数据+操作码传给Binder驱动

  4. 数据拷贝:Binder驱动将数据从Client用户空间拷贝到内核空间(只拷贝一次)

  5. 唤醒 Server:Binder驱动通知Server进程,Server通过mmap直接读取内核空间的数据

  6. 执行方法:Server执行对应方法,将结果按相同路径返回给Client

Binder=ServiceManager(黄页)+Binder驱动(快递员)+mmap(直达通道),实现一次拷贝的跨进程调用。

mmap

mmap 是一种内存映射文件的技术,能把磁盘文件直接映射到进程的虚拟地址空间。

让进程读写内存就像直接读写文件一样,省去了传统的 read() / write() 系统调用、减少一次数据拷贝和用户态与内核态的切换。

在 Android 里,Binder 用它实现一次数据拷贝,MMKV 用它实现高性能 KV 存储。

在binder中mmap的应用:Binder用mmap实现接收方的内核缓冲区和用户空间映射到同一块物理内存,数据从发送方拷贝到内核后,接收方可以直接读,省了一次拷贝。

在mkkv中mmpa的应用:MMKV 把存储文件直接映射到内存,读写数据就是直接操作内存,完全不需要read()/write()系统调用,所以性能极高。

Binder连接池

AIDL衍生通信方式。代理模式。

作用

将每个业务模块的Binder请求统一转发到远程Service中去执行,避免重复创建Service的过程。

解决问题

当多个业务模块都需要AIDL通信时,需要建立多个AIDL和Service,应用会很重量级,浪费系统资源。使用Binder连接池可以对Service进行复用,减少Service数量,将AIDL放在同一个Service中去管理。

工作机制

  • 每个业务模块创建自己的AIDL接口并实现此接口,不同业务模块之间不能有耦合,所有实现细节要单独开。

  • 向服务端提供自己唯一标识和其对应的Binder对象。

  • 服务端提供一个queryBinder接口,这个接口根据业务模块特征来返回相应的Binder对象给客户端

  • 不同业务模块拿到所需 的Binder对象就可以进行远程方法调用。

工作原理如下图所示:

图 Binder连接池的工作原理

选取IPC方式

名称

优点

缺点

使用场景

demo

Bundle

简单易用

只能传输Bundle支持的数据类型

四大组件间的进程通信

文件共享

简单易用

不适合高并发场景,且无法做到进程间实时通信

无并发访问情形,交换简单的数据是实时性不高的场景

AIDL

功能强大,支持一对多并发通信,支持实时通信

使用稍复杂,需处理好线程同步

一对多通信且有RPC需求

Messenger

功能一般,支持一对多串行通信,支持实时通信

不能很好处理高并发情形,不支持RPC,数据只能通过Message进行传输,因此只能传输Bundle支持的数据类型。

低并发的一对多即使通信,无RPC需求,或者无需要返回结果的RPC需求

ContentProvider

在数据源访问方面功能强大,支持一对多并发数据共享,可通过Call方法拓展其他操作

可以理解为受约束的AIDL,主要提供数据源的CRUD操作。

一对多的进程间的数据共享

Socket

功能强大,可以通过网络传输字节流,一对多并发实时通信

实现细节稍微繁琐,不支持直接的RPC

网络数据交换

Binder连接池

与AIDL类似,性能更好

实现有点复杂,需处理好线程关系

多个业务模块都需要使用AIDL,秀技术专享hhh

IPC基本需要序列化和反序列化,常用方式是Serializable和Parcelable

Serializable是java的序列化接口,使用简单但开销大,序列化和反序列化需要大量IO操作。

Parcelable是安卓中的序列化方式,使用于android平台,使用麻烦,效率高。

跨进程通信的缺点?

需要序列化和反序列化,不适合大数据传输和共享,不适合高性能、复杂数据和复杂操作。一般比较复杂的使用binder池和aidl,aidl也有比较多的限制。比较好的大数据共享是tx的mkkv,少了一次用户态和内核态的切换,效率快很多,  呀咩啰,学不完根本学不完

需要序列化和反序列化,不适合大数据传输和共享。

  • AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout。in表示输入型参数,out表示输出型参数,inout表示输入输出型参数

  • AIDL接口只支持方法,不支持声明静态常量。

  • AIDL的包结构在服务端和客户端要保持一致,因为客户端需要反序列化服务端中和AIDL接口相关的类。

  • AIDL方法是在服务端的Binder线程池中执行的,存在多个服务端同时连接时,会存在多个线程同时访问的情形,需要在AIDL方法中处理线程同步,可以直接使用CopyOnWriteArrayList来进行自动的线程同步。

  • 对象不能跨进程直接传输

  • 使用RemoteCallbackList来删除跨进程listener的接口,RemoteCallbackList是一个泛型,支持管理任意的AIDL接口。

  • RemoteCallbackList工作原理:内部有一个Map结构专门用来保存所有的AIDL回调,key是IBinder类型,value是Callback类型。Callback类型封装了真正的远程listener。当客户端注册listener时,会把这个listener信息存入mCallbacks中;当客户端解注册时,RemoteCallbackList会帮助我们遍历服务端所有的listener,找出那个和解注册listener和具有相同Binder对象的服务端listener,并把它删除掉。客户端进程终止后,它能够自动移除客户端所注册的listener,RemoteCallbackList内部实现了线程同步功能。

  • 遍历RemoteCallbackList时,beginBroadcast和finishBroadcast必须要配对使用

  • 客户端调用远程服务的方法时,被调用的方法运行在服务端的Binder池中,客户端线程会被挂起,如果服务端方法执行比较耗时,就会导致客户端线程长时间阻塞在这里,如果客户端是UI线程的话,就会导致客户端ANR。如果明确知道某个远程方法比较耗时,那么就要避免在客户端的UI线程中去访问远程方法。

  • 服务端方法本身就运行在服务端的Binder线程池中,服务端本身就可以执行大量耗时操作,这个时候不要再在服务端方法中开线程去执行异步操作。

5.7. 数据库

5.7.1. SQLite

使用步骤

  1. 继承SQLiteOpenHelper

  2. 实现其oncreate、onupdate的方法

  3. oncreate里面执行sql语句建表,onupdate里面可以执行sql语句更新表

  4. 通过getWritableDatabase和getReadableDatabase来获取数据库再搭配contentvalues、cursor等对数据库数据进行增删查改。也可把这个DatabaseHelper封装到contentprovider里面,通过contentprovider进行增删查改

也可使用room。

sqlite数据库使用demo

https://gitee.com/flying-guy/ds/blob/master/HelloWorldDemo/DatabaseTest/src/main/java/com/desay/databasetest/MyDatabaseHelper.java

通过实体类反射创建数据库sqlite

https://gitee.com/flying-guy/ds/blob/master/DatabaseTest/app/src/main/java/com/lwj/databasetest/MainActivity.java

5.7.2. group by和order by的区别

group by是分组查询,将同一组的结果聚合在一起。

order by是对查询到的结果根据排序规则进行升降序排序。

group by相似功能的有having,group by是分成哪些组,having是留下哪些组,having必须搭配group by。

5.8. JNI

java通过jni来链接到ndk使用一些.so库(c++、c),主要用于需要精细控制内存的高性能编程或者定制产品,比如游戏、音视频编解码、网络库、ai相关,比如ffmpeg,rtmp,ncnn、yolov8算法集成。提高代码安全性,因为反编译so库比较困难,这个没用过。

使用

开发流程

1、java声明native方法。加载动态so库和定义jni需要实现的方法。gradle文件里面配置路径

2、编译java源文件得到class文件通过javah命令到处jni的头文件

3、ndk里面实现jni的方法

4、编译so库在java代码中调用

注意点:jni方法参数声明必须遵循其相关规则。

demo

入门demo

rtmp推流

使用ffmpeg从mp3提取pcm数据

使用ffmpeg播放视频

NDK内存模式

jvm与ndk开发比较大的差异点是内存方面,也是因为ndk内存是手动管理少了jvm的gc算法啥的所以跑得比jvm快,相应的开发难度也越大一些。

jvm内存基于art,分为线程私有区和线程共享区,自动垃圾回收,不需要手动管理内存。

jvm内存分为

  • 虚拟机栈:

局部变量表(存储成员变量和方法传参里面的参数)

操作栈(存储要进行操作的变量,进行算数操作)

动态链接(获取类或者接口并将其组合到java虚拟机的运行时状态以便可以执行的过程)

返回地址(记录调用方法的地方,等方法执行结束以后要回到这个地址继续执行)

  • 本地方法栈:JNI调用的方法

  • 方法区:存储类模板、常量、静态变量

  • 堆:存储实例对象

  • 程序计数器:记录程序运行到了那一步,方便线程切换回来时继续上一次操作

ndk内存模型基于c/c++、linux,直接操作linux进程地址空间,内存泄漏和野指针风险高。

ndk内存分为5块:栈(线程私有,存储局部变量、函数参数,小块)、堆(进程共享,大灵活,主要内存区域)、数据段(全局、静态变量)、代码段(可执行指令)、其他(mmap映射区、内核态内存)

方面 JVM 内存与使用 NDK(C/C++)内存与使用
内存管理 自动垃圾回收(GC) 手动管理(malloc/free, new/delete)
内存区域 堆、栈、方法区等 堆、栈、全局/静态区、代码区
指针 引用(安全) 原始指针(可能野指针、悬垂指针)
内存安全 高(自动边界检查) 低(手动控制,易出错)
性能开销 GC 停顿、内存占用大 无 GC 开销,内存控制精准
线程模型 统一内存视图 依赖平台原生线程模型
开发效率 ⭐⭐⭐⭐⭐ ⭐⭐
内存安全 ⭐⭐⭐⭐⭐
性能控制 ⭐⭐ ⭐⭐⭐⭐⭐
实时性 ⭐⭐ ⭐⭐⭐⭐⭐
内存开销 较大(GC、对象头) 精细控制
调试难度 容易 困难

风险点:使用ndk开发,c++内存管理不当会造成内存泄漏,有的c++内存泄漏可能需要系统重启才能解决,比如:内核资源和跨进程共享内存。之前有个网络库某些情况下内存泄漏,需要重启手机才能解决。

ndk开发中常见问题。c++内存:内存泄漏、双重释放、野指针、缓冲溢出。jni引用、数据拷贝非法访问、死锁。

5.10 四大组件

activity、service、contentprovider、broadcast

5.11 平行视界是什么

左右折叠屏手机,一边一个活动页面。市面上的方案有easygo和embedding

5.12 软件共版

使用submodule、核心使用到Gradle 自定义构建变体(flavorDimensions /productFlavors)

5.13 项目升级

gradle 4.x->gradle 6.7.x

android.support包->androidx升级、build.gradle依赖升级

target35

状态栏和底部导航栏适配

6. 没听过的

Retrofit(网络)、hook(逆向)

7. 多媒体&音视频

7.1. 音频长短焦点

7.1.1. 音频焦点策略,丢焦点和恢复时机,优先级,混音排查方式

音频焦点策略,一般由厂商自寻根据安卓系统的Audio进行改造,主要是分长焦点和短焦点。出声音的需要根据自身源情况申请到焦点才播放(实际没有申请焦点也可以播放,只是一种不让多种源同时播的时候的混音的策略,因为一般系统可能由几十种音源,一些定制系统和业务会跟随用户操作和页面变化音源播放状态会变化,几秒内可能变化十几次几十次,如果没有遵循这个策略大概可能会同时播放很多音源造成混音,所以定制和遵循这个策略很有必要,这样才不会混音)

长短焦点和优先级定义也是根据源特点来的。长焦点一般是申请到就持续持有,声音暂停以后就主动释放。短焦点是申请到焦点,播放完后还给上一个音频焦点的所属源,上个源的收到焦点变化可以继续播放声音。优先级高的源可以抢占。

长焦点:播放音乐视频、电台、Carplay...

短焦点:短的提示语、打电话、倒车影像、开机...

混音排查。主要是看混音的俩音源是否是有音源焦点才开始播放的,最后一次音源焦点在谁那,没有申请到焦点就播放就是业务问题,焦点申请正常且只有一个音源有但是也混音了,需要另外一个业务排除。如果另外一个业务甚至连播放接口都没有调过就混音了就让Audio框架的人看声音咋跑出来了。

7.2. MediaPlayer

7.2.1. 创建流程、原理

7.2.2. 为什么一个MediaPlayer只能设置一个surface
  1. 框架设计如此,生产者-消费者模型,Surface是消费者,MediaPlayer作为生产者,只能向一个消费者发送数据,这种设计保证了数据流的高效和可控。

  2. 音视频同步和性能考虑,如果允许多个Surface,系统需要:复制视频帧到多个缓冲区(内存消耗翻倍),协调多个显示表面的刷新率,处理多个表面的渲染状态同步,这会显著增加CPU/GPU负载和内存占用

  3. 底层解码和渲染的绑定关系,MediaPlayer在底层会创建一个解码器(Decoder)和渲染器(Renderer),解码后的视频帧需要通过专门的视频输出通道送到Surface,建立这种通道是资源密集型操作,系统只能维护一个这样的连接。

当时问这个问题的场景是实现分屏小窗的问题,一个MediaPlayer无法同时在俩surface上渲染,如果想要解决这个问题,可以使用exoplayer,这个比较稳。还有可以使用openGL,或者尝试一些其他技术栈比如ffmpeg,这些没试过。

7.3. ijkplayer

7.4. MediaCodec

7.4.1可以配置的参数

主要是MediaFormat里面的参数可以配置。

    public void initDecoder(Surface surface) {
        Log.d(TAG, "initDecoder: ");
        try {
            mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
        } catch (IOException e) {
            e.printStackTrace();
        }
        final MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, 1080, 1920);
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1080 * 1920);
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //IDR帧刷新时间
        //dsp配置
        mediaCodec.configure(videoFormat, surface, null, 0);
        mediaCodec.start();
    }

MediaCodec使用生产者消费者模式,与MessageQueue有异曲同工之妙。

md3信息有哪些

主要MediaStore.xxx.Media.xxx里面几个静态类Video的信息,比如:

MediaStore.Video.Media._ID
MediaStore.Audio.Media.ALBUM
MediaStore.Images.Media.AUTHOR

id、path、duration、name、fileName、lastModified、size、bookMark、作者名、风格、

7.5. camera

使用和原理

7.6. ffmpeg

7.6.1. 使用流程调了哪些接口

7.6.2. 使用中有什么缺点

没在实际项目中用过。。。不同版本的接口差异太大,维护成本高。代码比较难看。

  • 据说rtmp拉流的时候网络不稳定隔几秒钟就会断开。后面ijkplayer基于ffmpeg优化的时候把这块优化了。除了拉流外ijkplayer针对FFmpeg在移动端的常见问题,做了大量预埋修复:

问题 表现 ijkplayer修复
Seek不准 拖动进度条后画面卡住 优化了关键帧查找逻辑
音频焦点 播放视频时来电话,声音混杂 自动监听音频焦点,暂停播放
Surface销毁 横竖屏切换时闪退 增加了生命周期感知和自动恢复
内存泄漏 反复进入退出播放页,内存暴涨 完善了资源释放逻辑
  • 播放崩溃率高,播放器建议选择exoplayer。

  • so库占包体极大小大,虽然可以自己裁剪,比起exoplayer始终还是占了包体积,除非是用到了播放器以外的功能。

7.7. OpenGL平时拿来干啥

加水印、分屏

7.7. 图片压缩算法

png、webp、哈夫曼压缩算法  //todo

平时看了什么源码?

recyclerView、ViewPager2、Glide、binder

8. 设计

8.1. 面向对象编程

对象:object、实体类、bean对象。聚合各个属性。

8.1.1 描述一下封装、继承、抽象

封装

8.1.2. 优点、缺点、不使用它有什么不好的地方、继承有什么缺点、怎么解决,类膨胀怎么解决

继承:只能继承一个父类,没有很多功能。父类改动影响子类:使用组合代替继承。

抽象:自定义view或者经常用到的baseAcitivty,但是实现的时候会东塞一个view西塞一个接口,对于view相关的不是很好用,代码流程不直观不好看,建议用在一些通用的公共能力上

继承:容易嵌套太深,限制层级<=3。

继承:类膨胀,容易被子类篡改,复杂情况下不容易看出程序走向造成失败。

使用泛型和设计模式针对有缺点的业务场景优化,有的时候可以使用接口代替

8.1.3. 重写和重载的区别

都是多态的表现方式

重写override:父子类,运行时多态。继承父类实现父类已有的方法,或者实现接口。用于扩展或改变父类行为。

重载:同一个类,编译时多态。指同一个方法名的方法通过方法的入参个数/类型来进行进行重载。用于拓展接口,提供同一操作的不同参数形式。比如:现在业务已经有一个功能的方法了,但是某一个新业务需要这个功能基础上再拓展一个参数来进行其他操作,这个时候又不想影响到其他不需要变更的业务,就可以新开一个同名不同参的接口给新业务使用。

8.2. MVC/MVVM/MVP

8.2.1. 有什么区别

model层(数据层,如网络请求、缓存等数据源,bean对象):都一样

view层(activity/Fragment、view组件):也一样

不同的在于controller层、viewmodel层、presenter层。

c层:

实现:一般就直接写在view里面,也有观察者模式的mvc实现。view监听到model数据回调的时候直接在view里进行数据更新操作。

业务逻辑:直接放在m或者v层。

v层职责:触发业务操作、接收数据变化、触发ui更新、可能还吃业务逻辑

v与业务层的耦合:高

p层:

实现:会持有view,p在m的数据回调的地方去调用view.xxx方法,然后v在xxx回调方法里面进行数据更新。

业务逻辑:放在p层。

v层职责:v需要实现一个p回调的业务接口,在里面进行ui更新

v与业务层的耦合:中,v需要实现一个接口,p依赖该接口

vm层:

实现:一般监听是用livedata或者推状态的观察者模式。不持有view,只持有数据,v层监听到数据改变自己就去刷ui了。跟观察者模式实现的mvc区别在于mvc的v有直接持有m层的数据监听和回调,mvvm的v没有,它是监听vm的数据变化。vm不感知v存在,可复用给任意相关v

业务逻辑:放在vm层。

v层职责:只转发业务操作给vm和监听vm的数据变化,业务逻辑放vm

v与业务层的耦合:低,v无需实现任何特定接口,只需注册观察者(或使用 DataBinding)

8.2.2. 只是影响UI刷新吗?hhhhhh

vm:数据驱动、生命周期感应、解藕

修改源数据,view直接就修改展示数据了。

生命周期感应, Activity 重建(如横竖屏切换)时,容易出现数据丢失。但是使用vm,acitivity只要没有彻底销毁,viewmodel就还在,数据也还在。

view和逻辑解藕,vm里面没有v的引用,mvc很容易内存泄露,这个不容易内存泄露。

8.2.3. 各自缺点

mvc缺点

功能一复杂全都写v里面了,v里面几千行的ui+业务+数据更新。出问题难排查,改个地方可能崩一堆。

mvp缺点

要为每个页面定义一个 View 接口,方法一多(showLoading、showError、showData…)接口就爆炸。

mvvm缺点

  1. 一些大型项目数据层级非常多,业务逻辑非常复杂、数据流复杂的时候会出现:数据监听失败、数据更新失败、一些操作导致会频繁ui刷新实际这个时候数据并没有更新。

  2. 层级变多、类变多。一般有的小功能单独放到一个包下,m、c 2个类就够了,标准mvvm需要3个类。

  3. vm可能会变得非常肿。需要拆分。

选择方式:1.follow团队已有的架构设计  2.简单功能、demo 使用mvc或者mvp就够了 3.大型项目,具备mvvm条件可使用mvvm,需要处理好复杂场景数据更新相关操作。

8.3. 设计模式

8.3.1. 讲一下常用的设计模式和应用场景

还要关联一下自己的业务场景讲。

单例(数据存在于整个应用生命周期、多个地方使用)

构造(Alertdialog、自定义view封装可变参数、glide、okhttp)

观察者(等待数据、被动通知、一对多通知数据、长链接消息、广播、eventbus、mvvm、adapter.notifyDataSetChange())

代理(aidl)

外观(音视频sdk、Mediaplayer、context)

解释器(定义语法规则+解析(写框架会用到)、pagemanager解析AndroidManifest.xml)

责任链(类加载双亲委托机制、事件分发、oa系统流程单流转)

工厂(bitmapfactory)

适配器(recyclerview)

策略者(切换不同的算法、Layoutmanager)

原型模式(clone、intent深拷贝)

模板方法(封装流程、activity/fragment生命周期)

8.3.2. 设计原则

8.3.3. 设计有什么用?你平时有用吗?

没什么用,我看到别人在用我就用(bushi) 完

8.4. 高内聚、低耦合

8.4.1. 为什么要高内聚低耦合?

为啥要解耦?

隔离变换、提高复用。避免一个地方改动引起其他地方也跟着改。

why:

  • 避免循环引用

  • 抽象出通用公共组件库避免重复造轮子和便于维护

  • 独立出某一个功能、服务给其他多个模块使用

  • 实际业务需求、方便协同开发

  • 避免单独一个类过于臃肿

  • 方便定位问题。正常出了问题需要排查,一般是需要大致定位是哪里出了问题再进行分析。基础能力还是业务能力?数据层?用户操作层?

8.4.2. 什么时候需要解耦?解耦场景

when:

  • 模块/组件解耦。大型项目协同开发。一个项目有超级多的功能时,多人协同开发,需要组件化模块化来解耦,壳工程、前后端分离、独立编译运行。

  • 业务和基础能力解耦。分清产品/业务方提的业务需求和基础能力。服务类,作为基础功能提供方,可能是全端或者多个业务同时使用,需要单独解耦开。

:基础能力为了某个业务需求,直接调用了业务的埋点接口。结果另一个业务使用时,埋点逻辑被错误触发。这是大部分基础能力提供方任意踩的坑

解法:基础能力只提供扩展点(接口回调、监听器),由上层业务注入具体实现。遵循依赖倒置原则

如:数据扫描、播放器、会话服务、皮肤服务、营销能力、播放器sdk、渲染基础能力...

  • 分层解耦。用户交互、数据、ui刷新解耦(常用)。避免业务逻辑写在界面里面导致代码重复率比较高。

如:数据需要被多个界面/组件共享(如用户登录状态),数据需要复杂的转换、组合、缓存(如从多个接口聚合数据),业务逻辑需要频繁修改并保证测试覆盖。这种就可以mvc/mvp转mvvm,把数据层和用户操作层单拎出来解耦。

  • 接口/参数解耦。复杂度高,对细节要求比较高的业务。需要大接口拆成接口。

如一个页面上有几十个数据展示,根据用户操作和业务规则需要在某一时刻对某个数据进行更新,则只需要更新这一个ui点的数据,其他地方不需要刷新(会闪烁),这个时候就需要播放器的每个信息源的接口低耦合。(为什么不用diff?1.常见diff操作进行比对比较耗时,低端机很明显。2.媒体功能复杂度非常高,diff往往写在业务代码里面很丑陋且这个逻辑本来就应该业务基础提供相关能力)

需要根据具体业务、具体情况来判断是否需要解耦,解耦的时候最好fellow整体设计风格、公司已有能力、语言框架已有能力和主流设计风格,如果是简单功能就不需要解耦了,简单功能解耦反而会引入大量的包、库、类,占资源多难维护。

8.4.3. 什么时候该解耦 vs 什么时候不

该解耦的信号

信号 说明
同一个功能在多处重复 抽公共库
一个类/模块经常被改动 说明职责不单一,要拆
改动一处导致多处出问题 耦合太紧
多人改同一个文件冲突频繁 需要模块化拆分
单元测试写不了 依赖太多,需要 mock

不该解耦的信号

信号 说明
功能简单且稳定 解耦引入复杂度,得不偿失
只有一处使用 不需要抽公共库
解耦后增加大量接口/回调 代码量翻倍,难维护
团队规模小、功能简单 过度设计

8.4.4. 解耦度量
指标 说明 理想值
代码行数 一个方法的复杂程度 < 80
类行数 一个类的代码行数 < 1000
扇入/扇出 被依赖数/依赖数 适中
改动影响范围 改一个类要改几个其他类 越小越好
依赖 被几个业务依赖 >=2个业务
协同 几个人同时开发 >=2个人
8.4.5. 举措

how:

层次 解耦对象 手段 例子
方法级 方法里面的携带参数表示的意义 拆分为不同方法、不同携参、枚举场景
代码级 类与类之间 携参、枚举、接口、依赖倒置、观察者模式、广播(pass性能太差报用) A 依赖 B 的接口,不依赖具体实现
模块级 模块与模块之间 组件化、模块化、路由 业务 A 不直接依赖业务 B,通过路由跳转
进程级 进程与进程之间 AIDL、Binder、ContentProvider、Service+外观模式 音乐播放器单独一个进程、sdk
服务级 应用与应用之间 隐式 Intent、Broadcast 分享功能调起微信
业务分层 业务和基础能力 一般分为base、common、xxx业务_module/组件啥的
代码分层 ui、交互与逻辑、数据源 mvvm、mvp、mvc
  • 外观模式可以让解耦的接口更加清晰,调用方只知道哪些功能可以用、比如sdk、mvvm、依赖倒置、资源解耦

8.5. 其他设计方式

不同业务除了mvvm、mvc、mvp、设计模式、设计原则、面向对象编程以外针对不同类型的业务还有其他的设计策略和架构风格。

8.5.1. 偏大型互联网app

面向对象编程、链式编程、mvp/mvvm、native壳+跨端组件业务、组件化/模块化开发、生产者消费者模式、观察者/构造/工 厂/解释器/适配器/单例/原型模式、场景切面、渠道、bundleRuntime、mock多app、线程池隔离、弱网降级读、动态化数据 和功能。很喜欢搞通用能力设计,最好一次改动后面再加需求不用改了。

8.5.2. 偏厂商类型的内置app

面向对象编程、mvc/mvvm、模块化开发、观察者/构造/外观/代理/适配器/单例/原型模式、生产者消费者模式、线程池隔离、多渠道、多app、subModule、音源策略。很喜欢搞软件共版,最好一套代码跑几十个项目那种。

8.5.3. 现代化编程

声明式ui+状态管理、异步编程、链式编程+闭包、mvvm

比如:鸿蒙arkts、react、kmp、flutter

链式编程:由责任链/构造者等基础设计模式组成。

状态管理:观察者模式。

8.5.4. 生产者消费模式

比如流式rpc、会话、涉及编解码的应用:dvr啥的、流媒体应该也会用、RabbitMQ

8.5.5. 线程池隔离

目的:

  1. 有规模的app一般都需要,特别是启动优化那块,不然一启动就噶或者请求个数据一直搁那转圈圈。需要线程管控、资源调度。

  2. 大型app启动的时候如果不同业务同时开线程去进行耗时操作,有可能整个app都在抢夺资源,抢夺资源失败的业务直接return没有处理线程任务。且数量太多app可能直接噶掉。

  3. 有的业务更紧急,有的业务不需要响应那么快。详见线程池配置方式。

  4. 提高启动速度和优化首帧耗时。

常见手法:

  1. 给线程设置优先级和调度策略,比如Thread.IO、Thread.RPC、Thread.UI、Thread.Urgency业务根据自己业务情况使用。

  2. 配置普通线程池,把业务需要切线程的几个地方同一的放在不同位置来切,比如启动的时候开一个线程池,所有需要在启动阶段切线程的业务调用都放在这个线程里面。

8.5.5. 抗弱网

互联网应用需要,特别是海外版本。还有一些要求特别高的业务场景,比如支付、下单。

核心思路是:在网络差、延迟高、易断开时依然能保证数据传输的成功率和用户体验。

普通业务策略

合理使用缓存。大部分业务可以使用三级缓存策略:磁盘缓存(SP)、内存缓存(LruCache)、全局缓存(单例)、成员变量。没网络时看到旧数据、弱网时先看到旧数据,避免用户看不到数据或者等待太久,三级缓存策略可作为兜底展示。

抗弱网

自动重试。失败/超时时自动轮询重试,需要配置最大尝试次数和添加关闭重试时机。

修改超时配置。弱网情况下超时时长稍微改大一点。

断点续传。大文件传输,流式数据传输啥的,传到一半网络断开或者网络不好啥的需要端点续传,这个具体根据业务规则和使用的组件来。

主动监听网络情况。网络断开的时候给提示,正常的时候恢复功能使用。

8.5.6. native壳+跨端组件业务

实现:外面一个native页面,里面一个或几个小卡跨端组件

用处:常用于动态化需求且性能要求高的业务,如营销组件、广告投放、feeds区。

原因:需要首开性能好或者当前情况下只能使用native作为容器,外面页面基本都是native写的,大部分组件都是native,服务端数据动态化已经满足需求,但是内部有一部分区域ui变更非常高,比如:营销区域一点点位置投放的内容可能千奇百怪,native不可能写几十套ui,这个时候就可以用跨端组件,一个小需求一个组件,你想banner还是竖行列表还是散列动画都可以动态发布支持。

8.6. 设计题

8.6.1 设计一个拖拽容器

其他技术栈类似,下面以安卓为例。

8.6.1.1 拖拽的业务场景类型、需求、难点分析
拖拽类型 核心需求 难点
列表item拖拽-补位 重排序、跨列表移动 触摸事件与列表滚动的冲突、目标位置计算
不规则view跟手拖拽 精准跟随手指、边界约束 触摸坐标与视图位置映射、不规则热区判定
视频进度条拖拽 一维线性拖拽、数值反馈 精确取值、与播放器状态同步
视频/图片预览页拖拽 手势驱散/关闭、跟手滑动 与ViewPager/滑动返回的冲突、动画衔接
浮窗拖拽(类似于微信浮窗)容器拖拽 全局浮窗、边缘吸附 跨Window、系统窗口权限、吸附动画
视频播放分屏小窗拖拽 双窗口区域限制、吸附 多窗口区域计算、与其他窗口的交互

共性需求:

触摸事件拦截与分发

坐标转换(屏幕坐标 → 目标坐标系)

边界约束与吸附逻辑

拖拽状态的视觉反馈(占位、阴影、动画)

拖拽结束后的位置回正/落点处理

与滚动容器的冲突解决

8.6.1.2 容器设计

因不同业务形态差异太大,且拖拽实现具体方式和侧重点都不一样。所以无法做到统一的一套拖拽方案。需要分场景实现。

列表item拖拽+补位动效。

这类一般是用来列表展示,用户可拖拽item删除item或者移动其位置,删除的时候后面的item需要补位动画上前,移动的时候其他item需要自动让开和补位,长按选中item有弹出动画,松手item动画回到目标位置,可使用安卓rv配套的ItemTouchHelper。

  1. 自定义一个接口ItemTouchHelperAdapter,里面有数据交换和数据删除的方法,用于响应数据交换和删除方法。

  2. 自定义一个SimpleItemTouchHelperCallback类 继承自ItemTouchHelper.Callback。该类需要处理以下操作

isLongPressDragEnabled(能否拖拽)、isItemViewSwipeEnabled(能否侧滑)

getMovementFlags(同来设置 拖拽移动,或移动删除)

onMove(拖拽时去回调adapter里的onItemMove方法)

onSwiped(左右滑动,回调adapter里的onSwiped方法)

onSelectedChanged(选中项操作,一般样式有改变,或者弹出动画)

clearView(释放选中项操作)

  1. 关联ItemTouchHelper和rv

 //关联ItemTouchHelper和RecyclerView
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mMyAdapter, true, true);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(mRecyclerView);
  1. MyAdapter实现ItemTouchHelperAdapter,在这里响应adapter的数据变化并刷新列表

    @Override
    public void onItemMove(int fromPosition, int toPosition) {
        //交换位置
        Collections.swap(dataList, fromPosition, toPosition);
        notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onSwiped(int position) {
        //移除数据
        dataList.remove(position);
        notifyItemRemoved(position);
    }

demo代码

任意不规则位置view跟手拖拽

自定义方式

命中检测、拖拽执行、手势仲裁、视觉反馈

在父容器onTouchEvent,ACTION_DOWN的时候查找这个point的x,y附近是否有view,view是否被选中,如果选中就在ACTION_DOWN的时候通过TranslationX/Y去改变该view的位置。

可搭配长按响应、动画等操作优化体验。

ViewDragHelper

也可直接使用安卓ViewDragHelper组件。

图片、视频等大面积页面拖拽

实现可参考下面的PhotoView

5.3.7 多点触控

一般是图片、视频用得多,手试捏合啥的

基础也是onTouchEvent,多指操作可以搭配其point来使用,来区分是哪个手指

event.getActionMasked(); //获取当前的多点触控触摸事件

event.getActionIndex(); //获取本事件对应的 pointerIndex

event.getPointerCount(); ////获取当前触摸点个数

event.getPointerId(int pointerIndex) //获取索引值对应的 ID 值

event.findPointerIndex(int pointerId)  //获取PointerId对应的PointerIndex

常见组件:

开源的PhotoView

8.6.2 设计一个通用的搜索组件

互联网的搜索组件

mvvm、跨端组件、定义给外部拉起的方式或者外部使用该组件的方式、业务自定义内部边界

架构设计

v层:使用acitivty/dialog/view/跨端组件等方式实现。具体内容有:搜索框、输入框内部的默认提示词、输入字符过程中的展示的搜索提示词、搜索过后输入框底部会有本地记录的搜索词、搜索热词、搜索结果页呈现(native+跨端+动态数据)、异常处理(搜索中/空白/网络异常/输入框字符限制/lru限制)

vm层:逻辑处理层。比如界面上用户在输入框输入字符,这个时候调用vm层的方法然后去m层查询搜索提示词,点击搜索同理。

m层:实体类、网络数据获取类、本地缓存(数据库)、sp缓存+LruCache

降级策略:搜索过的先去rpc网络数据查询和本地数据库查询,如果本地查到或者断网弱网啥先展示本地没查到就等待rpc网络数据反馈。等待过程中使用骨架图展示加载中。

使用方式:外部通过schema、或者service方式来拉起搜索组件。写个使用文档。

通用表现:拉起方式更加通用,如果需要更加通用可以抽出数据,让业务各自定制。抽出数据端上可以做到搜索结果页提供容器业务方自己画搜索结果页的ui和塞数据,数据层比如服务端的可以以业务为维度定义一些场景方便管理,自己的业务数据打通搜索的服务端的接口,这样通用性和可管理性更高。同时制定一些约束防止业务方数据量太大或者自定义ui过于复杂把界面整崩溃、提供同一的业务数据埋点方案,添加埋点监控性能

9. 性能

9.1 平时关注的性能指标有哪些?

启动耗时(应用/页面/首页)、冷启首帧耗时(onGlobalLayout)

响应/卡顿/掉帧(页面跳转/动画/音视频同步)、滑动/点击事件响应、FPS(app使用的流畅度)

内存大小、包体积

app存活时长(煲机)

崩溃闪退

CPU

耗电

耗流量

白屏、黑屏、卡死、温度

9.1.1.  耗时操作有哪些?

耗时操作:

  1. 高概率:rpc、网络请求、数据库操作、文件读写、IO、file、进程间通信、编解码

  2. 低概率:sp(commit同步耗时,apply异步大数据频繁读取耗时)、图片加载、嵌套布局、复杂布局、动态计算也有可能、线程切换、锁等待、死锁、解析大型数据、AB开关、注册kmp、短时间生成大量对象

表现:

高概率一般在主线程就噶了

低概率的运气好不会直接噶,但是会被线上用户/低端机/手机性能不好的时候跑出来,或者界面一卡一卡的、其他数据渲染慢

解决方法:

不同问题有不同的解决方式,一般是放子线程/异步调用,动画铺平,框架解,mkkv减少一次内存切换,减少页面布局层级或者xml的布局写到java代码里面放首帧后,有一点点耗时但是不是很耗时的更改调用时机放首帧后,

之前遇到过:

  1. 偶现肉眼可见的动画抖动,低内存车机,主线程new file。

  2. 低端车机煲机多媒体应用8小时,视频播放卡顿严重,音视频不同步。煲机后资源损耗太多带不起来音视频应用😅

  3. 网络请求(弱网)。

  4. 应用启动的时候切线程去用bitmapfactory加载了一次image,导致冷启时用户肉眼可见的空白了几百毫秒,还是高端机。。。其实这里放主线程比较好,因为这里不是大图加载,切线程对用户的影响可能更大一些。

这里不得提一嘴ios,安卓发现整个问题的时候,同一个业务ios在冷启的时候去加载了400多次image。。。优化后稳定优化了200ms启动时间,这么多年竟然都没发现,ios的性能真是优秀,我要转ios(bushi)

  1. AB开关,组件化的时候在类似于应用启动时高性能静态加载资源的地方使用了AB开关,启动耗时劣化了400ms被框架拎出来了

  2. 下游业务使用kmp,注册下游业务的时候启动耗时又增加了,拎到首帧后线上又出来anr,因为kmp链接到了tecal。。。然后又被框架拎出来

  3. 给xml里面多加了一层布局,首帧耗时增加几十ms。。。

  4. 为了实现复杂动画,动态计算css,导致安卓大部分中/低端机动画掉帧非常严重。。。公司的跨端组件跑不起来复杂动画。为什么要动态计算而不是帧动画?当然是动画太复杂只能根据业务规则来计算。。。

  5. 跟手滑的时候使用translateX,整个页面掉帧。。。

  6. 日志打印,一些常调用遍历调用的地方加日志多了容易卡。

9.1.4  卡顿问题排查及原理

表现:滑动卡顿、动画卡顿、点击响应慢、拖拽响应慢。如果超级卡就anr闪退了

原因:就是刚刚那些耗时操作做在了主线程或者后台线程把资源占完了,导致前台无法及时绘制渲染。比如:绘制次数太多点击事件就在MessageQueue 里排队,导致“点击没反应”。

原理:Android 系统每 16ms 需要刷新一帧(60fps),如果主线程的处理时间超过 16ms,这一帧就会被丢弃,用户就会感到“掉帧”。如果丢帧严重,就成了“卡顿”。

绘制渲染:performdraw->翻译成displaylist指令->renderthread调用skio/vuncal/opengl等GPU绘制->然后将renderbuffer给surfacefing,然后,vsync配合屏幕刷新显示。

卡顿就是帧率<  刷新率

帧率 (FPS)

每秒生成的帧数。

刷新率 (Hz)

每秒刷新的次数。

排查方式:看代码里面有没有耗时操作、内存正不正常。Systrace / Perfetto、Choreographer#FrameCallback、BlockCanary。

  • 应用层(代码逻辑):  BlockCanary能直接告诉哪一行代码(如 DB操作IO操作复杂计算)占用了主线程太久。

  • 渲染层(绘图指令): 使用 Layout Inspector 或 Profile GPU Rendering (GPU 呈现模式分析)。看颜色: 如果柱状图里的**红色(Draw)过高,说明 onDraw 逻辑太重;如果紫色(Layout)**过高,说明布局嵌套太深,需要用 <include><merge> 或者 ConstraintLayout 拍平布局。

  • 系统层(资源竞争): 使用 Systrace / Perfetto。这是排查“为什么卡顿”的终极工具。你可以看到 Choreographer 的每一帧绘制信号,如果看到 doFrame 后面紧跟着大量的 GC 或者 Binder 调用,你就知道是 CPU 资源被抢占了。

9.1.2.  包大小

有什么影响?怎么优化?

影响

互联网app:新app是影响app上架需要短时间压缩包体积,如果是很多年的老app包里面有很多功能包大小已经超了需要严格控制需求迭代的新增的包体积,不然发不了版

车载、厂商内置app:一般这类是将app作为内置应用,包体积太大,会影响内部功能使用,流畅度啥的,如果是低端机型那就有得搞了,因为一般低端机性能非常差,内存又非常小,用一段时间很容易内存就满了。

优化

在不影响用户体验的情况下进行优化,压缩。

  1. 混淆代码

  2. 图片、资源压缩。图片可以使用webp压缩、内置lottie资源找视觉压缩,基本能压很多。

  3. 代码绘制样式图片、动效。以便减少内置图片大小和减少图片加载时的空白时间。

  4. 资源改成线上资源。画不出来再尝试线上资源,图片、lottie、字体、视频

  5. 压缩xml文件。

  6. 删除无用/已下线功能代码

  7. 优化日志打印

  8. 代码裁剪。裁剪一些so库的代码只留有用到的资源,比如:ffmpeg。一些app没那么多动态化需求就没有必要引入跨端组件或者小程序的一些库了。

9.1.3  anr是啥?原理是啥?

当主线程在特定时间内没有处理完系统发送的事件,就闪退

  • 点击事件5秒没有响应

  • BroadcastReceiver: 前台 10 秒,后台 60 秒内未处理完 onReceive

  • Service: 前台 20 秒,后台 200 秒内未处理完 onCreate 等生命周期

排查手段

看trace、看log、看cpu是否负载过高

看 traces.txt 中的线程状态

  • RUNNABLE:正在运行,说明代码逻辑有问题,可能在做耗时操作。

  • WAITING / BLOCKED:被其他线程卡住了。重点看是谁持有了锁(查找 held by 关键词)。

查看 logcat 中的 ANR 信息,搜索关键词 ANR in,可以看到:

  • Reason: 系统提示是因为什么原因(例如:keyDispatchingTimedOut)。

  • PID/TID: 发生 ANR 的进程和线程 ID。

看 CPU 是否负载过高

  • 如果是 CPU 满载:说明代码在做极其复杂的计算(如循环加密、大型 JSON 解析),或者逻辑陷入死循环。

  • 如果是 CPU 空闲但主线程依然在 Wait:说明主线程在等待锁(Deadlock)或者等待 IO 操作完成。

10. 跨端

10.1. 公司跨端借鉴的语法和开源的语法的区别,编译生成的产物有什么

只支持少量标签、for循环只支持嵌套一层,不然性能不好、不支持webkit相关css属性、lottie默认属性没对齐、拿不到view的id给其做特殊操作、动态计算css属性非常耗时、给字体设置包边字体拐角处会有锯齿、lottie没有设置宽度时双端表现不一致、生命周期由native mock控制

.js .json,出问题可以在构建产物进行排查,看是否生成相关产物,没有生成的就是不支持或者代码写错了。

11. 技术方案相关

11.1. 怎么在OS桌面上展示一个视频播放小窗

service可后台播放视频,参考b站,缺点:无法播放页面和小窗口同时播放,因为MediaPlayer只能设置一个surface

11.2. 保活

双进程守护,一挂就再次马上拉起

service守护

还有一些在framework层做处理,没用过

11.3. 图片预览时怎么处理大图加载

需要预处理,裁剪和压缩,安卓艺术开发探索书上有。一般是采样压缩,即通过降低图片的分辨率来减少内存占用。
核心步骤
1.获取原始图片尺寸(不占内存)
首次调用 BitmapFactory.decodeXxx 时设置 inJustDecodeBounds = true,此时只解析图片的宽高(outWidth、outHeight),并不真正加载像素数据到内存。

2.根据图片容器宽高和图片素材宽高计算采样率 inSampleSize,让容器装得下图片
通过 calculateInSampleSize 计算一个 2 的幂次 的数值(如 1、2、4、8…),使得采样后的图片宽高恰好大于或等于目标宽高。
例如原始图 2000×2000,目标 100×100,采样率最终为 16,采样后图片变为 125×125。

3.按采样率解码
再次解码时将 inJustDecodeBounds = false,并设置 inSampleSize = 16。系统只会加载原图每 16×16 个像素中的一个像素,最终生成的 Bitmap 内存占用仅为原始内存的 1/256,从而避免 OOM。

特点
只降低分辨率,不改变图片格式,适合将大图显示到尺寸固定的 ImageView。采样率必须是 2 的幂(官方推荐),算法采用循环翻倍直到满足条件,保证解码效率。

/**
 * 图片压缩功能
 */
public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImageResizer() {
    }

    /**
     * 高效加载图片:将裁剪后的图片传给ImageView,降低内存占用,从而避免OOM,提高Bitmap加载时的性能
     */
    public static Bitmap decodeSampledBitmapResource(Resources resources, int resourceId, int resourceWidth, int resourceHeight) {
        //1、将inJustDecodeBounds设为true并加载图片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources, resourceId, options);
        //3、根据采样规则结合目标view所需大小计算出采样率
        options.inSampleSize = calculateInSampleSize(options, resourceWidth, resourceHeight);
        //4、将inJustDecodeBounds参数设为false,然后重新加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(resources, resourceId, options);
    }

    /**
     * 根据采样规则结合目标view所需大小计算出采样率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int resourceWidth, int resourceHeight) {
        //2、取出图片的原始宽高信息(outWidth、outHeight)
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > resourceHeight || width > resourceWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize >= resourceHeight)
                    && (halfWidth / inSampleSize >= resourceWidth)) {
                //inSampleSize按照2的指数倍递增,直到图片的宽高小于ImageView
                inSampleSize *= 2;
            }
        }
        Log.d(TAG, "calculateInSampleSize: inSampleSize --> " + inSampleSize);
        return inSampleSize;
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fileDescriptor, int resourceWidth, int resourceHeight) {
        //1、将inJustDecodeBounds设为true并加载图片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
        //3、根据采样规则结合目标view所需大小计算出采样率
        options.inSampleSize = calculateInSampleSize(options, resourceWidth, resourceHeight);
        //4、将inJustDecodeBounds参数设为false,然后重新加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }
}

decodeSampledBitmapFromFileDescriptor与decodeSampledBitmapResource的区别

对比项 decodeSampledBitmapFromFileDescriptor decodeSampledBitmapResource
数据源 文件资源(外部存储) 应用资源(res/)内部资源
密度处理 不处理,直接原始尺寸 自动根据屏幕密度缩放
采样基准 图片真实物理尺寸 经过密度缩放后的尺寸
典型用途 加载用户照片、大图 加载内置大图(如启动图)

11.4. 视频录制到一半把进程杀了,怎么拿到已经保存到本地的数据

传统视频录制需要在视频录制结束的时候手动封装成mp4,如果录制过程中打断录制,数据保存不下来会丢失。

解决方法:

1、使用一些流媒体技术,ts流,缺点:体积略大

2、录制缓存,定时刷新,缺点:消耗略大,如果时间间隔太长,中间时间端仍然可能会丢失

12. 其他

12.1 怎么做好一个软件,质量标准有哪些

  • no bug。no bug是风险控制和用户体验的关键,正常情况下bug越少越好。

前期进行充分的技术调研,掌握已有的代码演进路线。

写好系分,方案设计时需提前识别各类风险并通知到上下游,跟上下游(涉及到的服务端、前端、算法、产品、视觉交互、运营、测试)拉通对齐预期效果和具体实现效果。做好功能/数据监控,准备好回滚预案。

写代码的时候保证功能正常,做好容错,适配。除了保持正常的正向业务流程正常,基本还需要兼顾各种逆向交叉叠加中断恢复等异常case容错,数据获取不到/弱网/网络异常/锁屏/压后台,安卓系统/手机系统/分辨率/多语言/主题适配。

写完后自己根据业务场景多测试几遍,看代码分析功能走向正不正常。

  • 可拓展性强

一些通用组件单独抽出来形成组件库和工具类。不同业务的不同业务组件库不一样,case by case,一般超过两个不同地方都需要用到同一种能力就可以考虑搞成通用组件库。跟业务不搭关的用的地方多的就放到工具类

基础能力功能建设与营销功能建设分开,前面的更注重用户体验,功能稳定性,性能,推荐使用native技术栈,后面的更注重是否能快速迭代功能上去,满足产品营销对于某场活动的需求,推荐使用能动态发布技术栈。

该抽象的抽象、该细化的地方细化和解构。自己根据经验判断。

  • 性能好

  • 用户体验好

功能有用、功能正常运行、性能正常、界面信息正常展示、交互正常。感觉好多app都做不到。。。

13. 笔试题

动态规划+正则表达式

BM66 最长公共子串
REAL208 年终奖

怎么实现栈、队列,栈怎么转队列,队列怎么转栈

遍历二叉树

两线程交叉打印i,做在线程里

怎么合并2个链表

14. 项目

14.1. 挑一个参与感重或者有技术难点的项目讲讲,讲一下主要流程

排除太久的、排除没上线和刚上线的app、排除跨行的、排除有保密协议的。

问一下面试官他比较喜欢哪种类型业务:用户体量大的业务、复杂业务、复杂缓存基础能力、shi山代码怎么堆需求、复杂营销类型、纯技术类型、创新型、长链路业务,讲故事。还是喜欢一次性到位的完美架构和丝滑研发流程。

先star原则讲一下:所处背景(项目阶段、项目规模、面临挑战)、完成什么任务、采取什么行动、得到什么结果。

再具体讲讲做了什么事、遇到什么问题、怎么解决、为什么这么做怎么考虑的。重点讲业务架构/技术架构设计/功能流程/原理。有没有沉淀啥通用东西,让自己或者同事下次遇到类似的事情有更好的处理方式。

实际没有技术难点可以从以下几个方面入手:

1. 极致压缩工期 + 人手不足
讲你怎么拆里程碑、做优先级裁剪、并行开发、最小可用版本先行。

功能拆解、风险评估、排优先级保基础重要功能并尝试分批上。摇人、熬夜加班、尝试简化需求、使用跨端技术栈。2026可以使用ai啦,这种情况应该会比较少见了。

2.业务兜底、团队协作

带同事熟悉开发流程、写系分、评审系分代码。如果团队有同事写不出代码可以适当指导,还是写不出活又没人接只能。。。团队协作定好规范
3. 超重历史包袱、老旧技术栈兼容
讲怎么平滑兼容、渐进式重构、新旧架构共存、不影响线上业务。

改个需求提前理清楚相关的所有链路。上下游/交叉场景/分层搞的定制代码/分级回滚场景。
4. 非常规线上/业务疑难问题
讲问题定位思路、根因分析、临时止血方案、长期根治方案。

也就是下面的技术卡点。
5. 怎么拆分复杂业务/技术、长链路跨团队需求协作、怎么开发框架需求
讲需求对齐、接口契约、形成规范文档、并行排期、边界定义、冲突协调。消除隐形等待和重复决策。设置时间阈值。
6. 技术调研、技术方案设计 + 多方案对比 + 兜底预案
这是架构能力核心:为什么选A不选B、优缺点、容灾降级、兜底策略。基于已有技术资产和历史功能来进行选型和设计。
7. 技术选型思考
从业务体量、维护成本、团队上手、社区生态、未来扩展性多角度讲。
8. 容灾、降级、容错设计
风险未发生规避风险。异常case兜底、功能回滚。

风险已经发生需要马上解决将影响降到最小,并建立新的规范防止类似问题发生。
9. 扒源码、手搓自研组件、使用工具
懂底层、能造轮子、能解决框架解决不了的定制问题。

结合源码和业务代码,并使用工具profile分析实际表现定位问题。根据业务规则来搓仿原生组件相关的相关功能。扒google commit提交记录。
10. 推动落地现代化开发流程
工程化:规范化、CI/CD、代码规范、单元测试、分支管理、研发效能提升。
开发ai skills。

11.规范化软件开发流程并提效

好的开发流程可以很好的控制风险、从而开发又更多的时间专注于提升软件质量、优化用户体验、创新性想法落地,让开发和产品不受限当前某个需求,形成正向循环。

12.怎么将软件质量和用户体验做到完美

优良的性能、良好的架构设计、100%还原产品视觉预期效果并且该效果符合面向该产品定位群体的用户体验、便利或娱乐、功能无逻辑问题。

13.怎么内部解耦复杂业务、怎么提高代码复用率

技术卡点

如果没有技术难点,只有技术卡点。讲清楚为什么没有技术难点和遇到的技术卡点也中。

开发前期

开发前期进行了充分的技术调研和写demo,哪里有问题和容易卡点都会被拎出来单独解决,解决不了就换技术方案或者实现方式。全部试完以后还是解决不了或者成本太大会和影响较小会跟产品将清楚背景和原因,商量在业务设计/视觉交互侧做一些微微的妥协,如果平时是100%或者超预期还原产品预期,偶尔一两个小需求出现点用户无体感和体感不强和有问题的地方过于偏僻概率不高啥的一点点瑕疵,基本都可以接受。

开发过程中

所以实际过程中一年到头下来可能只有1、2个小点会有点问题卡点,基本无难点。

历史bug

平时有比较多的技术知识储备,基本遇到问题就当普通bug解了。

小的技术卡点

技术选型错误、历史包袱过重、机器性能太差叠加历史设计bug、下层耦合导致上层业务无法解耦导致的业务问题、没有标准的开发和预期规则、兼容性问题。

1.技术选型错误或者带来的问题:这个是最常见和最坑的。比如之前有的技术选型老板直接拍板跨端组件,但是跨端组件基础能力技术支撑不足、性能非常不好,业务需求相比跨端组件已有能力来说过于复杂。导致功能只能做到70、80%

跨端基础能力技术支撑不足的有:拖拽相关能力、导航栏定制。

跨端技术常见问题有:低版本os的ios schema解析不了非文字类型的表情包啥的导致跳转失败、lottie双端不一致、组件容器生命周期导致的一系列问题。

跨端技术性能不好:一页上各种根据业务规则来动态计算的多帧动画,动态计算css非常吃性能,频繁这么操作安卓机带不起来,直接掉帧、渲染失败空白、卡顿。加上应该是客户端业务还是跨端组件客户端自己写了内存泄漏的问题,导致一直内存泄漏,泄漏加剧该问题。

还有因为一个业务需要手搓一个跨端框架的,真是要命,劝君早点放弃,因为永远不知道产品提的需求有多难搞,跨端框架没有选好标准的使用方案,业务写起来简直灾难。如果非要这样搞建议使用更加通用的技术栈和使用标准,react、kmp、h5、小程序啥的。遵循比较现代化的开发标准和范式。

2.方案设计有问题+历史包袱过重

比较少见,一些特别的需求方案设计一般会拉其他资深开发工程师cr,他都看不出来,这问题谁来都不好使,会遇到是预期范围内的事情。

在一些用户量级和业务量级非常大的地方:需要去改底层容器增加一些基础能力来支撑业务功能,叠加历史包袱导致平时常见的解决方案都失效。什么安卓是个坑能掉多深是多深。解决方法是业务自己使用最基础的能力手写仿安卓原生组件来规避一些问题或者兼容已有的历史业务需求。手势与ui交互那块是最容易出问题的,常见需要手写的有vp1、仿rv、仿vp2+rv、仿indicator、仿抽屉

3.机器性能太差+历史设计问题

几年前的百元机一系列操作(各种使用、煲机)后跑几年前的多媒体项目

性能不好,哪里程序跑得慢点,卡在了没有架构设计只有各种标志位的if else里面出不来挂了。

根据业务规则解一下,后面提升一下设计能力做好容错处理。

4.下层耦合导致上层业务无法解耦导致的业务问题

也是多媒体。比如一个操作之后,下层返回数据上来,全耦合在一个接口里面,状态跟信息耦合在一个接口里面且没有办法区分开是哪种操作触发的状态还是信息更新,导致上层业务信息更新闪烁或者更新失败,不该更新的更新,该更新的被拦截。让下层业务自己改,下层业务自己都区分不开

5.没有标准的开发和预期

也是多媒体。除了音源管理是遵循os系统和车机系统的同一管理规则外,媒体其他表现都是比较看开发对业务的理解,正常app基本只用管ui展示对不对,但是多数媒体除了ui还带音源,再叠加上系统自带的其他几十种音源和车机自带的特有需求,音源触发界面,界面关联音源,叠加上其他业务交互,业务场景翻倍,比较复杂且叠加场景非常多,一个媒体app的case得上百种。什么时候播、什么时候暂停、什么时候恢复、什么时候更新哪一个元素、什么时候展示界面。每个人对不同场景下的表现都会有疑问。比如长音源媒体后台播放过程中被一个短音源打断,短音源快结束的时候用户又拉起另外一个带业务的长音源业务播放,长音源时各种用户操作以后拉起长音源媒体业务播放界面并播放该媒体源,再被其他业务抢音源焦点但是没抢页面焦点。。。各种套娃叠加后这个时候问其他业务停止播放后多媒体源是否需要恢复播放、播放界面是否需要恢复、信息是否需要刷新。还有1秒音源切换5、6次的时候混音啥的,实际播放源跟实际业务播放页面对不上啥的。

解决方法:除了既定的已有规则,其他遵循业务设计和历史效果,用户体验好就ok。

6.安卓兼容性/性能问题

从安卓几开始兼容,海内外各种机型。解决方法:拿到具体或者相似机型手机改代码调试出正常表现的代码。性能要求高的地方尽量选择原生技术栈,开发过程中不要劣化该地方的性能,如果这个地方性能太差影响业务开发需求就多尝试几个不同的技术方案。

7.框架需求

没错,接到有些需求偏整个大型app,需要直接改基础能力。改框架代码和推动app所有相关业务改造。---解决方案就是自己多尝试多思考,不会的拉个脾气好的框架开发问问。

Update-------------------------------------------------------------------------------------

java集合类

hashmap、concurrentHashMap、sparearray、

hashmap是否线程安全、多线程会咋样?与coccurentHashMap的区别,多线程用hashmap会有什么异常

异常处理,throwable是error还是啥

15. git

15.1. 平时用什么git命令?

15.2. git merge和git rebase的区别?

15.3. 如果提交了a b c d四个记录,想改成a c b d咋整?

使用rebase修改提交记录


****华丽的分割线********


怎么排查内存泄漏?线上?有什么场景?


怎么设计流式会话架构

怎么用数组实现linkedhashmap的功能

讲一下arraylist原理,无限增长会出现什么情况

hashmap原理

anr、卡顿问题排查及原理

讲一下binder使用和原理

aidl编程成什么文件

ipc和rpc的区别

讲一下单例模式

枚举方式呢?

讲一下DCL

syncorinazed原理

valitaile的原理

讲一下handler

播放器怎么避免多源混音


oom怎么排查?原理

个人有什么优势

架构系分文档有什么内容

从上到下挨个介绍项目和职责

怎么做到频繁切换团队、业务、技术、项目

怎么对接外部厂商的?会遇到什么问题及其解决方式

ai使用技巧、怎么避免ai幻觉

鸿蒙与安卓的区别

为什么要追求极致体验?为啥要高质量

产品提的需求违背原则咋整


架构系分文档有什么内容

ui设计:核心组件、组件拆分、复用、色值

复杂通用组件封装:背景、划分原则、共性(进容器)、非共性(留外面)、控制器传递约定、外部使用实例

埋点设计:核心类、接口,埋点方式、埋点常量定义,携带参数及其意义、上报时机、洗数据统一口径

接口设计:RPC接口或者该功能需要依赖的外部接口标注

数据模型设计:实体类、属性类型及其意义、如果有枚举需要说明具体类型和代表意思、为空怎么处理、嵌套多层则需要标注key在哪一层的位置及其规则、映射关系、类转换(如果需要)

时序图:核心功能时序图、流程图(如果是大系统就拆分成小系统,如果是跟外部依赖大需要把外部上下游也加上)

模块详细设计:功能说明、交互设计、数据(数据源、字段意义、出参入参、转换规则)、安卓/鸿蒙代码、设计思路

系统架构图:技术分层、业务分包、及其对应的说明、内部外部依赖关系、模块之间的关系、系统边界(属于该系统的职责,不属于该系统的职责)、接口边界

质量保障:可灰度、可监控、可回滚(回滚逻辑、回滚后的表现)、测试方案(功能测试、兼容性测试、性能测试、边界、交叉场景、标注易出错的地方、测试用例)

项目背景:背景、功能范围、参与人、相关文档、

风险与依赖:业务/技术/上下游风险、业务/服务端/上下游/基础模块/外部服务接口依赖、排期

鸿蒙与安卓的区别
  • 系统:类似于微内核、可以很好的对原子服务能力进行拆分,需要什么服务就薅什么服务进系统,比如一些鸿蒙设备不需要多媒体就不装这个服务进系统,需要摄像头就把摄像头装系统,而非安卓这种宏内核,所有功能集成在一起,需要装载一整个系统,虽然可以做到懒加载服务不过也是装在一起的。所以鸿蒙做生态这块特别厉害,非常生态设备

  • 设计理念:全场景分布式设计,软硬共享,比如手表可以调用手机摄像头实现摄像,实现硬件能力虚拟化共享

  • 业务侧感知:UI这块,鸿蒙使用现代化的arkts语言和架构设计。mvvm、声明式ui+状态管理、链式调用+闭包、异步+单线程、安全性语言(减少空指针、内存泄漏、并发的风险)

鸿蒙单线程+异步,不会卡顿,耗时操作使用异步挂起,执行期间主线程该干啥干啥,异步执行完了再回抛回来主线程继续操作,类似于安卓的协程。

安卓java代码不用协程,会出现多线程+ui可能等待被阻塞,会卡顿,使用锁线程啥的还经常用错内存泄漏卡顿啥的。

所以鸿蒙一般出现异步时序错乱的竞态(后台线程除外)、不会同时多线程修改读某个数据导致并发。解决竞态方法一般是使用标志位、取消任务的方式,注意使用标志位的时候记得取消该标志位不然会卡住不能再次执行任务。

  • 安卓的优点:开源部分随便改、生态圈大

讲一下单例模式

饿汉式、懒汉式、dcl、枚举、容器、静态内部类。

静态内部类:懒加载(Holder类只有在调用getInstance时才加载)、线程安全由JVM类加载机制保证、不需要volatile,性能更好

枚举方式呢?

天然线程安全、防止反序列化破坏单例、不需要任何同步。

枚举消耗内存资源大,安卓一般不用。

讲一下DCL

2次判空+一把锁+volatile

第一次判空是为了提高效率,多线程直接一个裸的锁效率低

加把锁是为了防止多线程同时来拿实例创建多个实例

第2次判空是为了方式对象new一半的时候跑进来,实例化过程中的重复创建,防止在锁等待期间被其他线程创建

volatile是为了方式对象new一半的时候不可见,指令重排序

syncorinazed原理

valitaile的原理


****华丽的分割线********


16. 别人的面试题

Android面试题(持续更新中)_android 面试题-CSDN博客

30 个 Android 面试高频问题及答案_android面试题-CSDN博客

Android 常见100道面试题(完整版)_android 面试题-CSDN博客

Logo

一站式 AI 云服务平台

更多推荐