【安卓面试】Android知识点记录

本文总结了Java和Android面试中常见的知识点,包括Java基础如反射、集合、线程、内存管理,Android部分如Activity启动模式、IntentService、崩溃捕获,以及并发、线程池和自定义View等。还涉及了一些框架、性能优化和Kotlin相关知识。

文章目录

前言

尽管大环境不是很好,但是我感觉这两年安卓的整体行情还是不错的。而随着大厂不断毕业以及技术的发展,对安卓开发的要求也越来越高,本篇主要是对遇到/收集的基础面试题进行了整理和回顾,可能个别不够全或者有错误也欢迎指正~
当然面试也会考察其他方面,对于部分也做了整理分享,核心还是个人学习笔记以及一些资料参考,具体学习最好还是看结合源码和资料学习
最后祝大家早日上岸~ 如果觉得有帮助可以点个👍或关注

Java基础

对反射有了解吗?

基础

  • 主要涉及到的类:Class类、Field类(类属性)、Method类(类方法)、Constructor类(构造方法)
  • 带有Declared修饰的方法可以反射到私有的方法,没有Declared修饰的只能用来反射公有的方法。
  • 访问私有方法、成员函数

使用场景

  • 组件化时,通过反射获取各个组件初始化类,进行初始化(实现了统一接口)
  • 使用ViewPager + Fragment 时,如果传入一个包含Fragment的List,会造成对Fragment的强引用,造成内存泄漏
  • 组件化读取配置文件进行反射
  • 一些灵活性比较高的代码比如动态生成的一定规则的类可以通过反射去获取
Class.getField和的区别,getDeclaredMethod和getMethod的区别
  • getField、getDeclaredField 分别是获取公共成员变量所有的成员变量
  • getMethod、getDeclaredMethod是获取到所有公共方法(包括Object类的)和获取到当前类的所有方法
类的初始化顺序依次是?

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

Java几种集合
  • List: List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
  • Set: 不允许重复的集合。不会有多个元素引用相同的对象。
  • Map: 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
对java的集合List有了解吗?
  • 对于随机访问 get、set ArrayList效率优于LinkedList,因为LinkedList要移动指针遍历;
  • 对于新增add和删除remove操作,Linkedlist要优于ArrayList,因为ArrayList要移动数据
  • Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
ArrayListLinkedListVector
底层动态数组链表动态数组
特性随机访问get和set性能高(Linked需要移动指针遍历)新增删除操作add和remove性能高(因为ArrayList要移动数据)(ArrayList在添加和删除的时候,底层是创建一个新的数组,而LinkedList却只要修改一下指针就ok了)支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢
使用查询多,使用ArrayList添加\删除多,使用LinkedList和ArrayList一样
对java的集合Map有了解吗?
MapHashMapHashTableConcurrentHashMap
底层数组+链表,数据量大时会转化为红黑树进行保存;线程不安全;Key值可以为空和hashmap差不多,但是key、value不允许为null,线程安全,效率较慢线程安全,ConcurrentHashMap采用了分段锁技术,没有同Hashtable一样锁住全部数据,效率较比HashTable高;key、value不允许为null
  • HashMap线程不安全,因此在扩容(创建新数组再重新计算hash)的时候多线程进行put可能会导致添加到旧的数组;这种情况应该考虑换一种数组结构
LinkedHashMap和HashMap的区别
  • LinkedHashMap继承自HashMap,是基于HashMap和双向链表实现的。
  • hashmap是无序的,LinkedHashMap是有序的,分为插入顺序和访问顺序。如果是访问顺序,使用put和get时,都会把entry移动到双向链表的表尾。
  • LinkedHashMap存取数据还是和HashMap一样,使用entry[]数组的形式,双向链表只是为了保证顺序。
  • LinkedHashMap也是线程不安全的
栈和队列的应用
  • 栈:例如遍历View树的时候,如果用递归,可能会造成栈溢出(或者其他递归场景),此时可以用队列/栈的方式改写 ,参考:遍历树
  • 队列:常见的生产者消费者模型,例如播放源源不断的音频等
String,StringBuffer与StringBuilder的区别
  • String 不是基本数据类型,而是一个对象
  • String 底层是一个final类型的 char[ ] 因此String的值是不可变的,每次对String的操作都会产生新的String对象;
  • 创建字符串的时候先查找字符串缓冲池中有没有相同的对象,如果有相同的对象就直接返回该对象的引用,如果没有相同的对象就在字符串缓冲池中创建该对象,然后将该对象的应用返回。
  • StringBuffer 和 StringBuilder 底层是可变的 char[ ]
StringStringBufferStringBuilder
不可变可变可变
线程安全,速度较慢线程不安全,速度较快
比较两个对象需要重写的方法
Java的强软弱虚
  • 强引用:在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
  • 软引用:在内存不足时会被回收(无强引用)。用于实现内存敏感的高速缓存。
  • 弱引用:只要GC回收器发现了它,就会将之回收(无强引用)。用于Map数据结构中,引用占用内存空间较大的对象。
  • 虚引用:在任何时候都可能被垃圾回收器回收(无强引用)。
Java 浅克隆和深克隆的区别
  • 浅克隆: 是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
    (子类对象还是指向同一个地址)

  • 深克隆: 不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。(引用的对象也进行了克隆)

  • 实现深克隆:重写父类的clone方法,在clone调用包含的子类的clone克隆方法

动态代理和静态代理的区别
  • 动态代理是** 运行时 **生成对应的一个新的类,这个类会实现相关的接口,并调用我们实现的InvocationHandler#invoke
  • 静态代理的代理类是写死的,可能会存在挺多重复逻辑
  • 动态代理原理:运行时动态生成代理类,调用到我们传入的InvocationHandler
并发部分
AQS 原理

AQS内部维护了一个CLH队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列sync queue里。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程

CountDownLatch 原理
  • 参考
  • 大致原理:利用AQS里面维护的一个volatile类型的整数state,每次调用countDown就把值减一,如果不为0则进入等待状态
  • 这个state也是通过CAS算法保证原子性
CAS和Atomic相关类原理

image-20220530233411041.png

  • Atomic相关类用到了JDK对CAS相关实现,保证了相关的原子性
  • CAS全称:Compare and swap,流程如图
  • 大致原理:利用处理器CAS指令,不断循环指令直到成功
  • 流程:执行操作时,与主存的变量进行比较,如果发现与主存的值不同,则重新取值,再次进行操作,直到值相同
  • 存在的问题与解决:
  • ABA问题:加上版本号进行判断(修改后又改回去了)
  • 性能消耗:可能存在大量重复流程而造成的消耗
  • 只能包装一个共享变量的操作(打包到一个对象)
  • API实现
AtomicMarkableReference : 有没有改过(解决ABA问题)
AtomicStampedReference : 改过了几次
死锁发生的条件
  • 多个线程竞争多个资源(线程数 >= 资源数)
  • 获取资源的顺序不对
  • 获取到资源后不能被抢夺释放
  1. 互斥条件
  2. 请求保持
  3. 不剥夺
  4. 循环等待

如何避免?

  • 改变资源获取顺序(统一先A后B)
  • 获取不到主动释放资源(Lock相关API)
ThreadLocal原理
  • 每个Thread 持有一个ThreadLocalMap,ThreadLocalMap中又持有一个Entry数组,这个Entry是一个key为ThrealLocal的的键值对,当存值时会获取到当前线程的ThreadLocalMap,再set值,相当于每个线程的变量都是本地保存的
  • 具体使用时,会通过Thread#currentThread获取当前线程实例,再取其成员变量进行赋值/读取 操作
Volatile原理
  • 会使用带LOCK前缀的一系列指令操作,将当前的缓存写会主存,同时使其他线程保存的副本失效
  • 待补充…
线程的状态有哪些?
  • NEW 新创建了一个线程对象。
  • RUNNABLE:可运行状态,正在JVM中执行,但是可能在等待CPU时间片
  • BLOCKED : 阻塞状态
  • WAITING : 等待状态
  • TIMED_WAITING: 具有指定等待时间的等待状态(调用sleep、wait、jion方法)
  • TERMINATED;:终止状态,线程执行完毕


图片来源及详情参考

线程的几个常用方法
  • start 开启一个线程
  • run 执行任务的具体逻辑
  • join 阻塞当前调用它的线程,等待join执行完毕,当前线程继续执行。
  • Thread.currentThread() 获取执行当前方法的线程
  • Thread.yield() 降低线程优先级,可能会使线程进入暂停状态
  • Thread.sleep(ms) 使当前线程在指定时间内休眠
wait和sleep方法的不同
  • wait()来自Object类,sleep()来自Thread类
  • 调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁
  • sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;
  • sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用
多个线程如何按顺序执行
  • 使用Thread的join方法
// 先执行 t1,ti执行完后再执行t2:

   // 在t2线程调用
   t1.join(); // 里面实际调用了wait方法 执行完后会调用notifyAll;此时t2拥有t1的锁,直到t1执行完

join 原理:里面也是调用wait方法

  • 加入synchronized关键字
  • 单线程线程池
线程交替执行

利用wait、notify

/*
 * 简单复习:
 * 1.wait和notify都是Object类的方法。
 * 2.wait和notify必须要在synchronized代码块中执行,否则会抛异常。
 */
public class WaitNotifyPrint {

    private static int count = 0;
    //两个线程竞争该对象锁
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new TurningRunner(), "偶数").start();
        new Thread(new TurningRunner(), "奇数").start();
    }

    //1. 拿到锁直接就打印。
    //2. 打印完,唤醒其他线程,自己就休眠。
    static class TurningRunner implements Runnable {

        @Override
        public void run() {
            while (count <= 100) {
                synchronized (lock) {
                    //拿到锁就打印
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    //打印完,唤醒其他线程
                    lock.notify();
                    //如果任务还没结束,就调用wait()让出当前的锁
                    if (count <= 100) {
                        try {
                            //自己休眠 放弃锁
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
线程池相关
  • 常见线程池
  1. FixedThreadPool

线程数量固定
线程处于空闲状态时 并不会被回收(除非线程池被关闭)
只有核心线程 且并不会被回收 可以更快的响应外界的请求
任务队列也没有大小限制
创建:Executors 的 newFixedThreadPool

  1. CachedThreadPool

线程数量不定(最大线程数可以无限大)
只有非核心线程
当线程池中的线程处于活动状态时,就会创建新的线程来处理任务,否则用空闲的线程
空闲线程有超时机制,时长为60秒 超过60秒的闲置线程就会被回收
任务队列是个空集合——任何任务都会被立即执行(SynchronousQueue() 简单理解为一个无法插入的队列)
适合执行大量的耗时较少的任务 当整个线程池都处于闲置状态 线程池里面的线程都很超时而被停止 此时几乎不占用系统资源
创建:Executors 的 newCachedThreadPool

  1. ScheduledThreadPool

固定的核心线程
无限的非核心线程 且非核心线程闲置时会被立刻回收
适合执行定时任务和具有固定周期的重复任务
创建:Executors 的 newScheduledThreadPool

  1. SingleThreadExecutor

只有一个核心线程
确保所有任务到一个线程中按顺序执行
任务之间不需要处理线程同步的问题
创建:Executors 的 newSingleThreadExecutor

  • 自定义线程池各参数含义

1、corePoolSize: 核心线程数量,线程池中会存在这么多个线程,当线程数量(包含空闲线程)少于corePoolSize的时候,会优先创建新线程,可以设置allowCoreThreadTimeOut=true来让核心线程池中线程也移除
2、maximumPoolSize: 线程池的最大容量,线程池中的线程数量不得超过这么多个,除非阻塞队列设置为无界的
3、keepAliveTime: 空闲线程存活时间,线程空闲超过这个时间的时候就会销毁(可以通过设置ThreadPoolExecutor#allowCoreThreadTimeOut使核心线程也可以销毁
4、unit: keepAliveTime的时间单位,分钟、秒等
5、workQueue: 线程工作队列,阻塞队列,线程池从这个队列中取线程,可以设置的队列类型(容量为:capacity):
ArrayBlockingQueue:有界阻塞队列,当线程数量n:corePoolSize <= n < maximumPoolSize 且 n >= capacity(队列大小) :创建新线程处理任务 当:n >= maximumPoolSize 且 n >= capacity 拒绝线程
LinkedBlockingQueue: 无界队列,maximumPoolSize不起作用,会一直创建线程
SynchronousQuene: 不缓存任务,直接调度执行,线程数超过 maximumPoolSize 则直接拒绝线程
PriorityBlockingQueue: 带优先级的线程队列
6、handler: 拒绝策略,线程数量达到maximumPoolSize时的策略,默认提供了4种:
AbortPolicy: 直接丢弃并抛出异常
CallerRunsPolicy: 线程池没有关闭则直接调用线程的run方法
DiscardPolicy: 直接丢弃任务
DiscardOldestPolicy: 丢弃最早的任务,并尝试把当前任务加入队列
7、threadFactory: 创建线程时使用的工厂,可以对线程进行统一设置,如是否守护线程、线程名等

image-20220531162444944.png

  • 执行
  1. submit 有返回值
  2. execute 无返回
  • 关闭线程池
  1. shutdown (尝试关闭线程池,关闭没有任务的线程)
  2. shutdownNow (尝试关闭所有线程,线程中断)、
  • 任务特性和线程池划分
  1. CPU密集:线程数设置为CPU核心数 + 1
  2. IO密集:线程数设置为CPU核心数 * 2
  3. 混合型:线程等待时间/线程CPU时间+1)*CPU核心数
  • 线程池异常
  1. 提交的runable异常:不会干扰到其他线程,如果核心线程数不够数量的话会新起一个线程
  2. 线程已满异常:由设置的handler 拒绝策略处理
  3. 线程池本身的异常

Android部分

LRUCache
ArrayMap/SparseArray
  • 作用:轻量级HashMap,通过数组避免创建大量对象(Entry),减少内存开销;查找使用二分查找,数据量大时会降低效率;KV可以为空
  • 实现原理
  • 两个重要的成员变量
  1. mHashes,int 数组,用来保存 key 的 hashCode。
  2. mArray,Object 数组,用来保存** key-value**,长度时 mHashes 的 2 倍。
  • 查找
  1. 通过Hashcode 进行二分查找hash冲突:判断key值对不对应,不对应的话再向前/向后查找相同hash值的key比较
  • 扩容:
  1. 小于4 则分配4,4-8 ,则分配8,其他的为1.5倍(因为会有一个4、8的缓存池);创建临时变量,进行数组拷贝
  2. 如果数组过大(大于8),且数据过小(1/3)时会触发降容
  • 缓存:
  1. 缓存: array 大小等于 4 或者 8 时,且相应的缓冲池未达上限时(10),则加入缓冲池
  2. 分配内存时,如果所需要分配的大小等于 4 或者 8,且对应的缓冲池不为空,则会从相应的缓存池取出 mHashes 和 mArray
RGB565 和 RGB8888的区别 / 图片大小压缩
  • 以RGB8888为例,32位( 8 + 8 + 8 + 8),每个像素 占四个字节
  • RGB8888 比RGB565 少用两个字节,节约内存(省去了透明度这个通道)
  • 简单场景使用采样率压缩(根据图片宽高和View的宽高 有一套算法)
    1. BitmapFactory.Options.inJustDecodeBounds 设置为true
    2. 获取到元素宽高信心
    3. 结合目标View计算采样率(一直对半直到View宽/高大于 图片的宽/高)
    4. BitmapFactory.Options.inJustDecodeBounds 设置为false 重新加载
Activity启动模式相关
  • standard 标准模式(默认)
  • singleTop 栈顶复用(栈顶如果有则复用 – 回调onNewIntent)
  • singleTask 栈内复用(栈内如果有则复用并清除上面的activity)
  • singleInstance 单例模式(全局唯一)
  • taskAffinity 属性
  • 为启动的activity指定任务栈(默认为包名所以要和包名不一样)
  • 搭配singleTask时有用
  • 如果设置了这个属性,则会前台任务栈则会切换到该栈,后续创建的activity也会在该任务栈
  • 应用场景

image.png

Activity onSaveInstanceState onRestoreInstanceState 调用时机
  • onSaveInstanceState:从android P(28)开始,这个方法将在onStop()之后被调用。对于版本较低的系统,这个方法将在onStop之前调用,无法保证与onPause的先后调用顺序。
  • onRestoreInstanceState :onStart和onResume 之间
Server 生命周期
崩溃捕获和日志上传
  • bugly SDK接入 常规日志捕获
  • 通过Thread 自带的 Thread.UncaughtExceptionHandler 在主线程设置一个CrashHandler 把异常写文件然后上传
  • 文件写入可以用MMKV(mmap),避免来不及写入
IntentService使用以及原理

使用:

IntentService 是一个抽象类,继承并实现 onHandleIntent(Intent intent),在onHandleIntent处理异步任务

原理

工作流程

  • 在Service的 onCreate 创建了一个 HandlerThread和该线程的Handler
  • 在onStartCommand 调用onStart,在onStart又使用第一步创建的Handler发送一个包装(带有启动信息的)Intent的Message
  • 在handleMessage,会调用onHandleIntent,之后会调用stopSelf

注意

  • 可以多次开启IntentService,但是一次只能处理一次请求,剩下的请求会被挂起,依次执行后,如果没有任务了就会调用stopSelf停止
  • HandlerThread 是封装了 Handler的线程,带有Looper

推荐阅读

SharedPreferences是线程安全的吗?commit和apply的区别?
  • SP是线程安全的,但是进程不安全
  • commit是同步操作,会返回结果,尽量不在主线程操作;apply是异步操作
  • 每次commit/apply都会把全部数据一次性写入到磁盘,即没有增量写入的概念 。 所以单个文件千万不要太大 否则会严重影响性能
Activity和Fragment之前通信
  • 创建时可以通过bundle,setArguments的方式
  • 消息总线
  • 接口定义
  • 如果使用了MVVM可以通过共用Activity对象创建出来的ViewModel
  • getActivity 获得对象进行通信
Fragment的生命周期

onAttach -> onCreate -> onCreateVIew -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach

  • onAttach() 在Fragment 和 Activity 建立关联是调用
  • onCreateView() 当Fragment 创建视图时
  • onActivityCreated() 在相关联的 Activity 的 已创建,在这可以getActivity
  • onDestroyView() 当Fragment中的视图被销毁
  • onDetach() 当Fragment 和 Activity 解除关联时

通过FragmentManager管理Fragment的生命周期:

只有再创建的时候加入返回栈,在退栈时才会一起销毁创建的fragment,否则退栈时不销毁fragment

Fragment 懒加载

分两种情况:

  • 在ViewPager 下,setUserVisibleHint这个方法可以判断可见,首次可见加载;offscreenPageLimit预加载的数量应设置为1
  • show()hide()方法下添加的activity,通过onHiddenChanged判断
简单的内存泄漏
  • LeakCanary
  • AS自带的profile,针对内存占用飙升时的内存对象以及业务代码进行分析
  • 复杂判定用MAT
  • 简单内存泄漏分析
Handler相关

Handler 是安卓里面用于跨线程通信的一个机制
主要使用为在一个线程创建一个Handler对象,重写其handleMessage方法,对接收的消息进行处理,而在其他线程则通过sendMessage或者sendMessageDelayed发送一个Message消息
优化:
发送的消息通过Message.obtain方法获取,这样可以减少资源浪费,不用每一次都new一个新对象
一般我们使用Handler对象都会创建一个匿名内部类,这样回默认持有外部类的引用,导致内存泄漏,解决方法是:通过创建静态内部类,并且传入外部Activity或者Fragment的弱引用,又或者是在生命周期销毁时移除Handler所有的消息,可以避免内存泄漏

实现原理:

在主线程里有一个looper对象,在主线程被创建的时候会自动的创建这个对象,并且这个looper会不停的从MessageQueue读取消息,而当我们发送Message时,会将这个消息加入到MessageQueue消息队列中,looper从MessageQueue取到消息后会回调到handleMessage方法对消息进行处理;

需要注意的是,子线程中默认没有looper,所以如果要在子线程中创建Handler,需要先调用 Looper.prepare() 方法将子线程变成一个looper线程,再调用 Looper.loop() 开始对消息的读取(死循环),这时创建的Handler会自动绑定子线程的looper,开始不停的从MessageQueue消息。或者通过HandlerThread直接创建一个带Looper的Thread

内存泄漏:

  • 使用非静态内部类会导致内存泄漏,将其改为静态内部类 + 弱引用的形式传入Activity
  • 在生命周期结束前移除所有的消息

ThreadLocal

线程局部变量,为每个使用该变量的线程提供独立的变量副本(获取looper时用到了)

MessageQueue是什么数据结构?

  • 单链表实现的队列

消息处理Callback的顺序

msg本身的callback - handler成员变量callback - handleMessage 方法

延迟消息处理

当异步消息触发大于当前时间,则计算轮询超时时间,然后进入阻塞状态;

如果有新消息到达,则会唤醒,重新计算

nextPollTimeoutMillis 为 -1 时,表明队列无消息

同步屏障

  • 默认的消息都是同步的,可以手动设置消息为异步消息,会进行优先处理(可以设置Handler为异步的)
  • MessageQueue#postSyncBarrierMessageQueue#removeSyncBarrier设置和移除同步屏障,但是一般应用层不调用这两个方法,需要对一些消息进行优先处理时可以将消息设置为异步
  • MessageQueue#next方法中检测到有同步屏障消息时,则会开启一个while循环过滤出异步消息继续执行
  • 源码跟踪和参考
主线程Looper.loop()进入无限循环,为什么不会造成ANR
  • Android应用是由事件驱动,looper.loop()不断接收事件,Handler不断处理消息,如果没有消息,那么主线程就关闭了,应用也退出了。所以不是Looper.loop()阻塞了主线程,而是在处理事件的时候超过一定时间造成的ANR;
  • MessageQueue.next 会一直读取Message,直到有消息返回Message;而Looper.loop方法会调用MessageQueue.next获取Message,如果没有消息了就返回为NULL,退出循环

主线程中的Looper.loop()一直无限循环为什么不会造成ANR?

  • 线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
  • 主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。
  • 子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
  • ANR标准:activity - 5s ,BroadCastReceiver - 10s , server - 20s ,ContentProvider - 10s
子线程looper

new 一个Thread,在run方法里面写三行代码:

Loop.prepare; 

Loop.loop; 

输出一行log。 //  Q: 这行log会不会执行。

不会,loop方法 实际上进入了一个死循环 不断的去读取MessageQueue的消息(next方法)

Android各版本适配

Android 6.0 —— SDK 23

  • 动态权限申请

Android 7.0 —— SDK 24(7.1 是 25)

  • 新版V2签名 打包只勾选V2版本会在7.0以下显示未安装
  • 跨应用共享文件(拍照相册等会涉及),需要用到FileProvider

Android 8.0 —— SDK 26 官方文档

  • 通知渠道适配:可以创建通知渠道组(可选)、渠道ID,发通知时指定渠道ID
  • 服务创建的适配:处于后台的应用(无可见的Activity、无可见的前台服务、无被其他应用关联)不可以启动一个后台服务;在后台的应用创建的后台服务需要用**startForegroundService()启动一个服务,并且在5s内在服务调用startForeground()**将应用推到前台,否则会ANR错误
    对于周期性的任务,官方推荐以“作业”的方式(JobScheduler);替代IntentService:JobIntentService(JobScheduler使用:推荐参考
  • 广播的适配:禁止在其清单中为隐式广播注册广播接收器

Android 9.0 —— SDK 28

  • 默认禁用http,不允许明文流量( application标签下:android:usesCleartextTraffic=“true” 或者定义一个xml标签: android:networkSecurityConfig=“@xml/network_security_config” )
  • 需要禁用 Apache HTTP 客户端
  • 启动前台服务需要加上权限

Android 10.0

  • 私有目录改版(可以通过配置做到暂时不适配)
跨进程传递数据,为何要序列化?
  • 转换成字节数组,方便传输
  • Parcelable、Serializable,Parcelable是开了一块共享内存,其他进程可以通过这块内存读出字节流并恢复
Android 自定义gradle插件
  1. 工程的buildSrc Module,实现plugin接口并进行相关的配置,在apply方法实现相关的逻辑
  2. 这里可以做的包括对变体的操作过滤、配置检查、资源文件更改、利用transform API进行字节码操作
  3. transform大概流程:通过对jar包等文件、本地代码的查找找到目标文件,进行修改后将工作流传给下一个任务
  4. 修改字节码的工具包括 ASM、javassist
Lambda在ASM有什么不同
  • JAVA8 对lambda 的支持是新增了一个invokedynamic 指令,但是安卓早期是不支持java8的,所以为了兼容处理对lambda 做了脱糖处理——以匿名内部类的形式而不是静态方法
  • 虾哥的文章
场景设计题
不使用分段加载,如何展示一个大图?
  • 这里答了使用压缩后的图片展示
  • 压缩算法 采样率压缩
两种动画 平移和旋转,平移后留下的视图可以点击
  • 这里我答了组合动画 + 平移后addview
拉到1000条数据,自定义View,怎么绘制效率高
  • 通过滑动的距离和屏幕的大小,只绘制当前展示的item
如果要排查加载大图的图片,用ASM怎么做
  • 找到通用的hook点和提前准备好相关的工具类方法,在加载前或者加载时插入相关逻辑,如果超过一定大小可以报个日志之类的(这里没回答实现的细节,网上有挺多方案)
反馈SDK出现问题,如何排查
  • 这里答了找到触发的入口,触发时进行上报或者日志打印等
Framework相关
创建Binder的时机
  • fork出子进程(App进程后),会关闭子进程的socket(与Zygote),然后经过一系列的方法调用(nativeZygoteInit等方法)启动Binder
进程fork时为什么用socket不用binder?
  • Zygote进程没有binder,fork创建App进程时,会创建Binder
  • Binder 中存在一个Binder线程池,是多线程的,如果使用多线程进行fork的话,因为fork只会fork当前线程,可能会造成死锁
Binder如何做到一次拷贝

Linux 使用虚拟内存寻址,Binder借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映 射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝

应用启动流程 && Activity启动流程
  • 文章参考
  1. 大致流程原理图
  2. 代码跟踪调用
  • 大致原理
  • Launcher -> AMS -> Zygote --fork-> app进程(存在IApplicationThread Binder服务,接收AMS消息;AMS通过 app调用的AMS#attach 方法 将appproxy 传递给AMS服务,之后AMS通过这个proxy去管理应用)
  • 如果应用进程不存在,在ActivityStack.startSpecificActivityLocked 方法这里会进行判断,不存在则会通知Zygote(Socket通信) fork一个新进程,新进程进行一系列的初始化后在ZygoteConnection.handleChildProc 这里反射调用了ActivityThread#main方法,进入应用初始化流程
  • 大致流程
  1. app进入AMS:Activity#startActivity -> Activity#startActivityForResult -> Instrumentation#execStartActivity --> 通过名字获取到相关服务进行启动(这里的服务-名字 对象的信息,是在ActivityThread#BindApplication 时通过传递过来的)
  2. AMS处理 : AMS#startActivity -> 权限判断、进程判断、栈管理(冷启动则fork进程,然后将IApplicationThread对象attach到AMS服务) -> 通过ApplicationThreadProxy 跨进程传递回APP进程
  3. ApplicationThread通过Handler处理AMS发过来的消息 -> ActivityThread#performLaunchActivity 等等
  • AMS与APP互相持有对方的代理对象

在这里插入图片描述

  • 大致流程图(两次Binder通信)

在这里插入图片描述

View相关
View绘制流程
安卓事件分发
  • 大体流程

Activity - > Window -> ViewGroup(DecorView) -> View

  • 概览
  • 一层层分发判断处理,如果不处理会返回上层(View视图树)
  • ViewGroup会进行 分发/处理 ,如果不拦截,则会分发下去给子View,子View会调用View#dispatchTouchEvent 进行处理;如果拦截事件,则是自己处理(也是调用到View#dispatchTouchEvent 方法)
  • View#dispatchTouchEvent 事件处理优先级:TouchListener -> View#onTouchEvent -> ClickListener
  • 核心方法
  • dispatchTouchEvent:核心在ViewGroup中的实现,里面对分发做了大量的逻辑判断;View的dispatchTouchEvent是实现了对事件的消费
  • onInterceptTouchEvent(ViewGroup):是否拦截事件
  • onTouchEvent:对事件的处理
View事件分发 down、move、up等

参考 Android事件分发机制详解:史上最全面、最易懂
从activity的dispatchTouchEvent->phoneWindow->frameLayout->viewGroup->View

三个关键方法:

  • public boolean dispatchTouchEvent(MotionEvent event)
  • public boolean onInterceptTouchEvent(MotionEvent ev)
  • public boolean onTouchEvent(MotionEvent event
dispatchTouchEventonInterceptTouchEventonTouchEvent
用于进行事件的分发,如果事件能传递到当前View,此方法一定会调用存在于ViewGroup中,在dispatchTouchEvent方法中调用,判断是否拦截某个事件用来处理点击事件
返回值:受当前View的onTouchEvent和下一个View的dispatchTouchEvent影响true代表拦截返回值:代表是否消耗此事件

关系伪代码

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    if (onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else {
        consume = child.dispatchTouchEvent(event);
    }
    
    return consume;
}

在事件分发机制里主要有dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三个方法,分别的作用是分发事件、拦截事件、对事件的处理;
对于事件的分发:

  • Activity -> ViewGroup:
    事件分发从Activity开始,传递到ViewGroup再到View,当产生一个事件时,调用Activity的dispatchTouchEvent,里面调用了getWindow().superDispatchTouchEvent(ev),这里其实调用了mDecor.superDispatchTouchEvent(event),而mDecor是一个DecorView对象,DecorView的父类是继承FrameLayout,所以实际上是调用类ViewGroup的dispatchTouchEvent,这里将事件从Activity传递到ViewGroup
  • ViewGroup对事件的处理:对于一个根ViewGroup,当产生一个点击事件时,此时会调用dispatchTouchEvent方法,然后再dispatchTouchEvent方法里面会调用onInterceptTouchEvent方法,如果onInterceptTouchEvent返回true,那么这个ViewGroup的onTouchEvent方法会被调用,事件分发停止;
    如果onInterceptTouchEvent返回false(默认),则会将事件传递给子元素,子元素会调用它的dispatchTouchEvent,直到事件最终被处理;
  • ViewGroup -> View :
    如果子元素是View,则会调用子元素的onTouchEvent方法,onTouchEvent的返回结果就是dispatchTouchEvent的返回结果

对于事件的处理:
对于最后分发到的View,如果他的onTouchEvent方法返回false,那么会回调他的父容器的onTouchEvent。直到Activity
如果设置了OnTouchListener,那么会先进入OnTouchListener的onTouch方法,如果onTouch方法返回false,则会继续回调到onTouchEvent,如果返回true,则不会回调到onTouchEvent方法。并且OnClickListener也是处于onTouchEvent方法中的,由此可见他的优先级其实是最低的

LayoutInflater#inflate 的 attrachToParent true 是什么意思

这个参数代表是否把这个渲染的View与parent关联,如果parent为空且为true则会给他指定父布局(相关属性layout属性会限制),主意一个View只能有一个父布局,重复会报错

View的post方法

也是调用了 mAttachInfo里面的handler里面的post

getMeasuredWidth和getWidth有什么区别
  • getMeasuredWidth 是测量的值,getWidth是真实的值
  • 前者是在onMeasure 后产生,后者则是在onLayout后产生
LayoutInflate.inflate 流程
  • 解析最外层的XML布局
  • 调用createViewFromTag生成View(通过Factory、Factory2、privateFactory,反射创建)
  • 不断解析子View然后重复(递归调用,创建流程同第二步)
自定义View需要注意的几个方法
  • 初始化new对象应放在构造函数或其他方法,如果放在onDraw频繁new对象会影响性能
  • 自定义属性需要注意回收
  • onMeasure 根据测量模式设置wrap_content
  • onSizeChange 重新赋值获取的宽高
  • onDetachedFromWindow 移除动画相关
  • 滑动冲突处理(内部、外部拦截法)
更新View的几个方法

1、requestLayout: 当view确定自身已经不再适合现有的区域时,该view本身调用这个方法要求parent view重新调用他的onMeasure onLayout来对重新设置自己位置。特别的当view的layoutparameter发生改变,并且它的值还没能应用到view上,这时候适合调用这个方法。必须是在UI线程中进行工作()
2、invalidate: View本身调用迫使view重画。必须是在UI线程中进行工作。比如在修改某个view的显示时,调用invalidate()才能看到重新绘制的界面。
3、postInvalidate: View本身调用迫使view重画。在非UI线程中进行
4. 以上都会走一遍View的绘制流程,不同的是会根据标志位从而判定是否重新layout/draw

AAC 方面
Kotlin相关
Kotlin 伴生对象
  • 伴生对象在编译后会生成一个静态内部类;
  • 加了@JvmStatic注解,才是真正的静态成员
Koltin inline 标识的方法
  • 编译后会把该函数的代码直接“插入”到调用的地方,减少调用开销;注意该方法不能够太复杂;
  • 原因:方法的调用需要入栈出栈等操作是有一定性能消耗的,直接内联可以减少消耗
Kotlin扩展函数原理

反编译成java后可以看到是生成了一个静态方法,第一个函数是对应的对象

Kotlin扩展函数适合使用场景
  1. Java对象的方法封装一个kotlinDSL写法
  2. 拓展三方SDK
  3. 常用方法拓展(toast等)
作用域函数的区别?(let、also、apply等)
  • 返回值不同(also和apply返回本身,可以进行链式调用,其他(let、with)返回lambda表达式结果)
  • 对象引用的不同(this、it)
kotlin == 与 === 的区别

==比较的是数值是否相等, 而 ===比较的是两个对象的地址是否相等

kotlin空安全原理

kotlin生成的class文件会帮我们判断是否为空,如果为空则不调用

Kotlin协程相关
  • 几个常见的类
  • Job 和 SupervisorJob:Job是启动协程返回的一个管理类;协程可以嵌套,默认如果内部的子Job出现异常,会连带取消父Job和其他关联的子Job的运行,但是SupervisorJob中的子Job是相互独立的,比如常见的lifecycleScope
  • CoroutineScope,协程作用域,里面持有一个CoroutineContext;
  • lifecycleScope 分析
  • Kotlin协程的挂起和恢复
  1. 协程实际上是一个线程框架,当协程被挂起时调度器会帮我们切到另一个线程去执行,之后可以帮我们切回来;提供 “非阻塞式” 的写法,这个非阻塞事实上是切到其他线程。
  2. 协程在执行完后会帮我们切回原先的线程,这也是为什么suspend为什么需要在协程执行的原因 —— 有挂起才会有恢复
  3. suspend 这个关键字不会真正实现挂起(做到一个提醒),而是withContext 这个挂起函数
  4. 使用场景:耗时操作(网络、IO)
  • Kotlin协程和线程的联系
  • kotlin协程实际上就是切线程,是kotlin提供的一套操作线程的工具
  • 可以使用 “非阻塞式”的写法去写阻塞代码
  • Kotlin取消协程
  • 根据返回的job对象进行取消
  • 协程作用域CoroutineScope进行取消
  • Kotlin自动取消协程与生命周期绑定
  1. ViewModelScope开启的协程,会在销毁时一并取消,具体实现是定义了一个 CoroutineScope并传入了 SupervisorJob()_,_且用Closeable包装起来存进一个HashSet,在ViewModel#clear方法会遍历这个HashSet,如果Closeable的接口就会调用,最后是取消了这个作用域内的协程
  2. lifecycleScope:注册监听了生命周期,如果处于销毁状态则会取消
  • 协程的withContext和async的区别
  • async返回的是一个Deferred类型,是Job的子类,可以说是带结果的job(并行)
  • withContext适用于协程作用域内切换协程环境的一个函数(串行)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值