安卓 面试八股文整理(原理与性能篇)

安卓面试题整理,关于一些原理、底层和性能相关的考点。大概是1-3年水平。本人菜鸡,如有错漏欢迎留言

1. apk打包过程(7步)

1.资源文件(主要xml)通过aapt打包编译成二进制文件.arsc,生成R.java

2.aidl文件通过aidl工具打包成java接口

3.R.java、源码、java接口通过javac编译器编成class文件

4.三方库和class文件使用dx或者d8工具变成dex文件

5.编译后的二进制资源文件、不需要编译的文件(图片、动效)、dex文件通过apkbuilder打包apk

6.使用jarsigner或者apksigner进行apk签名(V1 V2 V3 V4签名)

7.zipalign进行4字节对齐

2. 安卓整体层级架构

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Linux 内核层(Linux Kernel)​​ 作用:作为系统最底层,提供硬件抽象和核心系统服务,是Android与硬件交互的桥梁。 |
| 硬件抽象层(HAL, Hardware Abstraction Layer)​​ 位于内核层之上,标准化硬件接口,屏蔽不同厂商硬件的差异 |
| 系统运行库层(Libraries & Android Runtime)​ 含原生C/C++库和Android运行时(ART) C/C++: Bionic​​:Android优化的C标准库,适配嵌入式设备。 多媒体处理​​:Media Framework支持音视频编解码(如H.264、MP3)。 图形渲染​​:OpenGL ES(3D图形)、Surface Manager(多窗口合成)。 数据存储​​:SQLite提供轻量级数据库引擎。 网络与安全​​:WebKit(浏览器引擎)、SSL(加密通信) Art AOT编译​​:安装时将DEX字节码预编译为机器码,提升运行效率。 内存管理​​:改进垃圾回收(GC)机制,减少卡顿。 多线程优化​​:与Linux线程机制结合,支持高效并发 |
| 应用框架层(FrameWork) 四大组件和系统服务 |
| 应用层 每个应用运行在独立进程,由art管理,通过应用框架层访问底层功能 |

3. 底层交互

3.1 java层native方法如何和c/c++函数绑定

静态注册:native函数命名Java_包名_类名_方法名,当Java层调用native方法时,JVM会根据命名规则在加载的SO库中查找对应的Native函数

动态注册:通过JNI_OnLoad函数和RegisterNatives方法,在运行时主动注册Java方法与Native函数的映射关系

3.2 JNI如何实现数据传递

java层和native层数据类型定义有些不同,所以数据交互要通过jni,先转成jni的数据格式,再由jni根据要传递到java层或者native层转换成对应可识别类型。

3.3 JavaVM和JNIEnv

​​ JavaVM代表Java虚拟机实例,一个进程内唯一,负责管理虚拟机的生命周期(如启动、销毁)和线程绑定。通过AttachCurrentThread为线程分配JNIEnv,通过GetEnv获取当前线程的JNIEnv,并协调跨线程的资源管理。封装虚拟机核心操作(如内存管理、类加载),提供线程安全的全局接口。

​​JNIEnv每个线程拥有独立的JNIEnv实例,不可跨线程共享。它提供线程本地环境,用于操作Java对象、调用方法等。包含函数指针表(如FindClass、CallObjectMethod),直接支持Java与本地代码的交互。

3.4 C和C++的JNIEnv区别

C中JNIEnv是JNINativeInterface的指针,使用时需要解引用访问函数表并传递env参数,c++的JNIEnv是一级指针,不用传递env参数。(隐含this指针)

cpp 复制代码
        (*env)->FindClass(env, "java/lang/String");  //c风格

        env->FindClass("java/lang/String");   //c++风格

4. WMS

WindowManagerService管理的整个系统所有窗口的UI。是 Android framework层系统核心服务之一,为所有窗口分配Surface。客户端向WMS添加一个窗口的过程,其实就是WMS为其分配一块Surface的过程,一块块Surface在WMS的管理下有序的排布在屏幕上。Window的本质就是Surface。

WMS管理Surface的显示顺序、尺寸、位置,窗口动画。

WMS是派发系统按键和触摸消息的最佳人选,当接收到一个触摸事件,它需要寻找一个最合适的窗口来处理消息,而WMS是窗口的管理者,系统中所有的窗口状态和信息都在其掌握之中。

5.Activity、Window、DecorView以及ViewRoot之间的层级关系

Activity → PhoneWindow → DecorView → ViewRootImpl

每个Activity包含一个PhoneWindow,每个PhoneWindow包含一个DecorView,每个DecorView 对应一个ViewRootImpl。

Activity不直接控制视图;window作为视图的承载容器,提供窗口管理策略(如标题栏、背景等),通过WindowManager将视图加载到屏幕上;DecorView作为整个视图树的根节点,通过setContentView()设置布局文件;ViewRootImpl连接WindowManagerService和DecorView,负责View的三大流程(measure、layout、draw)处理,输入事件分发和界面刷新。

6. Decorview什么时候可见

​​onCreate()调用setContentView()加载布局到DecorView的content区域,此时还没到显示;

​​onResume():Activity进入前台后,系统调用ActivityThread.handleResumeActivity(),在此方法中:将DecorView通过WindowManager.addView()添加到窗口。ViewRootImpl首次执行performTraversals(),完成视图树的测量、布局和绘制。视图绘制完成后绘制结果通过SurfaceFlinger合成后,DecorView内容才真正显示到屏幕。

简单来说流程是这样子:onCreate()-> onStart()-> onResume()-> (系统开始安排并执行UI绘制)​ -> 页面内容显示到屏幕。

官方文档说Activity在onResume时变为"可见",更多指的是其逻辑状态。此时Activity已获得用户焦点,准备好接收输入事件,但视觉内容的渲染需要时间。

7. AMS /ATMS

AMS是系统的引导服务,是 Android framework层系统核心服务之一,应用进程的启动、切换和调度、四大组件的启动和管理都需要AMS的支持。AMS的工作流程,其实就是Activity的启动和调度的过程,所有的启动方式,最终都是通过Binder机制的Client端,调用Server端的AMS/ATMS的startActivityXXX()系列方法,在安卓10以后,管理activity的工作从AMS挪到ATMS,其它三大组件管理还在AMS。

8.app启动流程

用户点击Launcher(桌面应用)中的应用图标,Launcher通过startActivity()发起一个包含目标Activity信息的Intent(如包名、主Activity类名)。

Intent传递:Launcher进程通过Binder IPC将启动请求发送至系统核心服务ActivityManagerService(AMS),安卓10之后是ActivityTaskManagerService (ATMS处理)

AMS/ATMS检查目标应用进程是否存在,若进程不存在,AMS通过Socket向Zygote进程发送创建新进程的请求(冷启动),ATMS会指示AMS去做。若进程已存在(如应用在后台),AMS/ATMS直接复用现有进程,跳转至目标Activity(热启动)。

创建Task与ActivityRecord:AMS/ATMS为Activity创建记录(ActivityRecord)和任务栈(TaskRecord),用于生命周期管理。(ActivityRecord必定创建,taskRecord根据 Intent 中的标志位(如 FLAG_ACTIVITY_NEW_TASK)、Activity 指定的 taskAffinity属性以及当前的系统状态(如是否存在相同 affinity 的任务栈)来决策是否需要创建一个新的任务栈,或是将 ActivityRecord 添加到已有的某个任务栈中。)

Zygote通过fork()复制自身预加载的虚拟机实例,生成新应用进程,继承预加载的Java类库和资源(如classes.dex)以加速启动。(是Android系统的核心进程之一,扮演着应用进程孵化器的角色,负责快速创建和管理所有Android应用进程及部分系统服务进程。)

进程初始化:新进程执行ActivityThread.main(),初始化主线程(UI线程)、消息循环,并通过Binder向AMS发送attachApplication请求,完成进程与Application绑定。

AMS/ATMS通过Binder 调用应用进程 ApplicationThread的 bindApplication方法,封装成message丢到主线程消息队列中等待处理,handler处理此消息时,系统实例化应用的Application类(或自定义子类),调用onCreate()进行全局初始化(如数据库、SDK初始化)。

通过应用进程向 AMS/ATMS 报告 attachApplication完成,AMS/ATMS便会通过 Binder向应用进程发送scheduleLaunchActivity请求,主线程通过反射创建目标Activity实例,依次调用生命周期方法。

9. PMS

PackageManagerService(简称 PMS),是 Android framework层系统核心服务之一,处理包管理相关的工作,常见的比如安装、卸载应用等. 其提供一个应用程序的所有信息(ApplicationInfo)、提供四大组件的信息、查询permission相关信息、提供包的信息、安装、卸载APK。

10.SharedPreference的apply和commit的区别?commit 一定会在主线程操作嘛

commit提交到数据库,从提交数据到内存,再存入Disk中都是同步过程,中间不可打断。有返回值通知是否成功,可以在子线程调用,但是由于同步特性会阻塞调用线程。

apply方法提交到内存中,而非数据库,所以在提交到内存中时不可打断,然后通过工作队列机制异步写入磁盘,不会有相应的返回值。

SharedPreference通过synchronized加锁保证线程安全。sharedPreference只有第一次获取需要加载,后续会复用,文件越大读取时间越长,所以在 onCreate()或 UI事件中首次访问SP易引发ANR,SharedPreferences初始化本身是异步的,但主线程访问未加载完成的SP会阻塞。​

在不需要立即知道保存结果,特别还是主线程调用sp写入,这种情况优先使用apply,避免潜在的主线程阻塞风险,必须确保数据确实成功保存到了磁盘,并且需要根据保存结果(返回值)立即执行后续操作时,应该使用 commit。这种情况较少见,例如在服务或后台线程中进行关键配置的保存,并且后续逻辑严重依赖此配置是否持久化成功。

11. Android的缓存机制

LRU缓存原理:LRU缓存使用linkedHashMap实现,在构造linkedHashMap是将第三个参数指定为按访问顺序排序,每次put会将数据插入队尾,每次get会把数据移动到队尾,这样子一直没有访问的数据就逐渐移到队头;

创建LRU缓存需要指定缓存大小,一般为当前可用内存1/8,必须重写sizeOf方法计算每个缓存项的大小,可以重写entryRemoved处理缓存项移除事件;当缓存满了之后会从队头开始移除数据,调用trimToSize进入循环开始从队头移除数据直到缓存占用的空间小于最大缓存空间。

所有修改操作(put, remove, trimToSize)都使用synchronized同步。

12. Glide库

12.1 glide缓存层次( 四级缓存机制****)****

第一层是活动资源缓存,使用弱引用集合存储,配合引用队列监听回收事件,引用归零移入LRU缓存;

第二层LRU内存缓存,底层基于LinkedHashMap,按访问顺序排序,缓存满时移除最近最久不使用的资源,被移除资源可能加入Bitmap复用池;

第三层磁盘缓存,分两种,资源类型缓存:存储解码后的图片,再次使用时无需重复解码;​​原始数据缓存:存储从网络/文件直接获得的原始数据,避免重复下载;基于DiskLruCache。作为持久化存储,它保存的是原始数据的文件副本。一旦存入,除非根据LRU策略被淘汰、手动清除或因签名变更失效,否则它会一直存在,不受内存中资源状态变化的影响;

第四层Bitmap复用池,复用已释放的Bitmap内存,减少内存分配/回收带来的性能抖动,同样采用LRU管理,当Bitmap从内存缓存被移除时加入复用池,属于享元模式。

12.2 Glide缓存工作流程

​​ 1. 生成缓存Key:根据URL、尺寸、变换参数等生成唯一的EngineKey

  1. ​​检查活动资源缓存:通过loadFromActiveResources()查找正在使用的资源

  2. ​​检查内存缓存:未命中活动资源缓存则通过loadFromCache()查找LRU缓存

  3. ​​检查磁盘缓存:先查找资源类型缓存(解码后的图片),未命中则查找原始数据缓存

​​ 5. 执行实际加载:所有缓存未命中时,从网络/文件加载原始数据

  1. ​​缓存回填:新加载的资源同时写入磁盘缓存与活动缓存

12.3 Glide内存回收

弱引用+引用计数:活动资源使用弱引用,通过acquire()和release()管理使用状态

​​自动回收:当资源不再被强引用持有时(引用计数归零),GC可回收弱引用资源

​​队列清理:引用队列配合后台线程定期清理已被GC回收的资源项

13. ANR

13.1 anr是如何分析的,什么情况会有anr

1.主线程5秒内未处理完用户输入事件(如点击、滑动) 日志关键字:Input event dispatching timed out.

2.前台广播的onReceive()未在10秒内完成,后台广播为60秒 日志关键字:Timeout of broadcast BroadcastRecord.

3.前台服务的生命周期方法(如onCreate())未在20秒内完成,后台服务为200秒 日志关键字:Timeout executing service.

4.ContentProvider的publish()操作未在10秒内完成。

可以分析日志文件找关键字判断是什么问题,然后访问/data/anr/xxx 的traces.txt文件查看是具体什么原因,系统会记录异常的位置、CPU和内存当时的使用情况,通过查看日志基本就能判断问题所在。另外接入腾讯bugly也可以线上监控anr发生,可以很容易定位问题。(adb bugreport 命令也可以直接把所有内容导出,然后找到FS/data/anr,具体可以根据日期来确定哪一个文件)

常见原因,主线程耗时操作、主线程被锁或者binder通信阻塞。

13.2 ANR触发原理

例如当我们开启一个服务,当系统启动Service时,会在ActiveServices.realStartServiceLocked()方法中触发以下逻辑:在调用Service的onCreate()前,通过bumpServiceExecutingLocked()方法发送延时消息到Handler。消息类型为SERVICE_TIMEOUT_MSG,携带目标进程信息(ProcessRecord)。

若Service在规定时间内完成生命周期方法(如onCreate()),系统会通过以下步骤移除延时消息:Service的onCreate()执行完毕后,移除SERVICE_TIMEOUT_MSG消息。

如果没有在规定时间执行完成,handler会处理SERVICE_TIMEOUT_MSG,用appNotResponding()记录日志并弹出ANR对话框。

13.3 Anr监控原理

一般有两种监控方式,通过监控事件处理到完成的耗时判断是否卡顿或者anr;

另外可以通过监控Choreographer,向该模块注册frameCallback监听对象,同时开启一个线程循环记录主线程堆栈,每次vsync事件doFrame通知回来时循环注册监听对象,间接记录两次vsync事件的间隔。

监控消息执行事件的方法缺点在于如果是复杂堆栈可能很难找出耗时函数;腾讯matrix使用字节码插桩方式,可以统计每个方法的耗时,所以我们可以很容易找到哪个方法耗时过长,而不用从堆栈信息一个个碰运气。

线上监控要通过拦截SIGQUIT信号来感知ANR事件。由于系统默认屏蔽了SIGQUIT信号(只有Signal Catcher线程能接收),监控代码需要先解除对该信号的屏蔽。然后注册自定义的信号处理器(signal handler),在捕获到SIGQUIT信号后,执行ANR信息收集(如获取主线程堆栈、当前上下文信息等)并上报。之后必须将信号重新转发给系统的Signal Catcher线程以确保系统能正常生成完整的ANR trace文件,不影响系统的原有逻辑。

14. 内存

14.1 安卓app内存模型

Java堆是JVM中最大的线程共享内存区域,主要作用是存储所有对象实例和数组。无论是new关键字创建的对象(如Object obj = new Object()),还是数组(如int[] arr = new int[10]),均分配在堆内存中。由虚拟机管理,内存溢出会报oom。

Native堆,由C/C++代码(如JNI、NDK库、系统库)分配的内存。通过malloc/free等方式分配和释放。典型使用场景包括:图片解码、音视频处理、第三方SDK等。不受Java GC管理,需要开发者手动管理内存分配和释放,因此容易发生内存泄漏。

方法区(代码区)是JVM内存模型中的线程共享区域,核心作用是存储类元数据、静态变量、常量及编译器编译后的代码,含c++代码。其中,类元数据包括类的结构信息(类名、字段、方法、接口)、运行时常量池(编译期字面量与符号引用);静态变量的生命周期与类一致,随类加载而存在,随类卸载而回收。方法区的内容由JVM在类加载时初始化,并通过垃圾回收机制(如卸载不再使用的类)管理内存,是类信息与静态资源的集中存储区域.

栈区,分为虚拟机栈和Native栈。虚拟机栈为每个线程分配的栈空间(一般为1MB~2MB)。用于存储局部变量、方法调用等线程私有的数据。Android应用中的每个线程都有自己独立的栈空间。栈空间的大小与应用中运行的线程数量直接相关。线程数过多会导致栈内存消耗大,从而影响整体内存使用。

Native栈由内核管理,专门为native方法服务的。当 Java 代码通过 JNI调用 C/C++ 编写的本地函数时,这些本地函数的执行信息(如局部变量、函数调用链、返回地址等)就保存在 Native 栈中。

Java程序计数器是线程私有的内存区域,作用是记录当前线程正在执行的字节码指令地址。每个线程独立拥有一个程序计数器,确保线程切换后能恢复到正确的执行位置。其主要功能包括:控制指令执行顺序(顺序执行时自动加一,指向下条指令);支持分支、循环、跳转等流程控制(修改计数器值为目标地址);处理中断与异常(保存当前计数器值,处理完成后恢复)。程序计数器是JVM中唯一不会发生内存溢出的区域,因为其仅存储指令地址,不分配额外内存。当执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果线程正在执行的是一个Native方法,那么这个计数器的值为空。当线程开始执行Native方法时,其执行流程的控制权从JVM移交给了操作系统和底层硬件。此时,操作系统线程和CPU的程序计数器(PC指针)开始发挥作用,记录和执行Native代码的机器指令。

14.2 Android系统为app分配多少内存

Android java堆限制,一开始安卓为java堆分配的内存初始堆大小由 dalvik.vm.heapstartsize定义(通常较小,如 4MB--16MB),随着使用过程内存,art分配内存失败时会先尝试gc,仍然不足可能尝试扩展内存,但不会超过dalvik.vm.heapgrowthlimit。我们通过 ActivityManager.getMemoryClass()拿到的就是这个值,当java堆占用内存到达此数值就会oom。不同手机和配置的厂商对于dalvik.vm.heapgrowthlimit配置不一样,我们可以在androidManifest.xml声明 android:largeHeap="true",堆上限提升至 dalvik.vm.heapsize的值,ActivityManager.getMemoryClass()拿到的值有可能就会变成dalvik.vm.heapsize,但是是否生效由系统决定。

​Native内存独立于 Java 堆,不受 Java 堆限制,但受进程总内存约束。

Linux 内核通过cgroup(控制组)机制为每个 Android 进程分配独立的内存资源池,这是进程总内存的硬上限(java + native + 一些显存)

要扩展app可使用的内存上限要不就是多进程,要不就把一些java堆的东西移动到native层申请,因为java堆内存相对比native少很多。

14.3 内存抖动

内存抖动是指在短时间内大量对象被创建又迅速被回收的现象。这种现象会导致:

1.频繁GC,Android的GC会暂停主线程,造成UI绘制卡顿。(一秒可能多次触发gc)

​​2.内存碎片化:老年代中的标记清理算法会在大量对象创建并销毁后留下内存碎片,可能导致后续大对象申请内存时产生OOM。

15. 引用

15.1 四大引用:强引用,软引用,弱引用,虚引用

强引用不会回收;

软引用内存充足时保留对象,内存不足时由GC回收,在抛出OOM前,JVM会优先回收长时间未使用的软引用对象;

弱引用只要GC运行,无论内存是否充足都会回收弱引用对象,有效防止因疏忽导致的内存泄漏,适合存储非关键性临时数据;(weakhashmap,glide)

虚引用get()方法永远返回null,仅用于对象被回收前的通知,必须与引用队列配合使用,用处:资源释放跟踪,替代finalize()方法的更可靠方案

15.2 引用队列

引用队列是配合软引用、弱引用和虚引用使用的重要机制:

​​工作原理:当引用指向的对象被回收后,引用对象本身会被加入关联的队列

​主要用途:感知对象被回收的时机,执行后续清理工作,实现更精细的内存管理。

例子:WeakHashMap自动清理;统计对象存活时间。

16. 内存回收

16.1 Jvm判断垃圾的方法

jvm垃圾回收主要通过两种方法标记对象是否可回收,一个是引用计数,即对象被引用则计数加一,取消引用就减一,没人引用就可以回收;二是GC roots可达性算法,这也是目前主要使用的算法,其通过从GC Roots开始,使用图遍历算法(如深度优先搜索DFS或广度优先搜索BFS)遍历对象引用图,找到所有可达对象,并将它们标记为"活跃"状态。标记通常通过在对象头部设置标志位来实现。非活跃的对象可以回收。

(现代jvm在标记时会使用并发的标记-清除算法,可以同时执行标记和清除,使用三色标记法,初始标记:标记从GC Roots直接可达的对象,通常会短暂暂停所有应用线程。

并发标记:与应用线程并发运行,标记所有可达对象。此时,对象可能从白色变为灰色,再从灰色变为黑色。

最终标记:处理并发标记期间产生的引用变化,通常会有短暂停顿。这是为了确保在并发标记过程中新产生的引用关系能够被正确处理。

清除阶段:回收未被标记(即白色)的对象)

16.2 Jvm GC roots回收垃圾的方法

遍历堆中的所有对象,回收那些未被标记(即"不活跃")的对象。这些对象被视为不可达,可以被垃圾收集器回收。垃圾回收也有几种可选算法,一个是遍历堆直接回收不活跃对象,缺点是会有两次扫描和内存碎片;第二种是标记时把活跃对象移动到新的内存块,回收时直接把旧块清除;三是标记时做整理,把活跃对象都移动到内存块一端,回收时另一端清理掉;四是目前虚拟机用的,将堆区分不同的类型,按对象存活特点分到不同区,每个区使用不同算法。

新生代经常性回收,使用将存活的对象移动到新内存块,然后回收旧内存的方式;老年代很少回收,使用整理的方式,将需要回收的对象整理到内存一端,进行回收。

16.3 为什么要使用静态常量等做gc roots

GC Roots是垃圾回收器遍历对象引用链的起点,它们必须本身是"绝对存活"的引用,且存在于不被GC管理的内存区域(如栈、方法区),从而确保可达性分析的正确性和效率。

常见gc roots有虚拟机栈中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(即Native方法)引用的对象

16.4 Lmkd

低内存终止守护进程,采用epoll方式监听linux内核psi内存压力等级的触发事件,并根据psi内存压力等级及进程优先级(oom_adj)来选择目标进程并查杀,缓解系统内存的压力。(AMS 通过 Socket 向 lmkd 发送进程的 oom_score_adj,lmkd 维护哈希表记录所有进程的优先级)

17. 内存泄漏

安卓的内存泄漏可以分为上层Java层的内存泄漏和底层C/C++(Native层)的泄漏,Java层的内存泄漏和我们平时所理解的C/C++语言的内存泄漏有点不同。在C/C++中,我们通过new/mallco等函数开辟堆内存,返回指向内存的指针,如果没有主动释放,就会导致内存泄漏;在java中有GC机制,new出来的对象在所有引用都失效后会自动被gc回收,java的内存泄漏实际上是程序逻辑中应该回收,实际上意外被长生命周期对象引用导致未进行回收。( 在c/c++中泄漏是借了忘记还,java中是借完以为还了实际没还)

17.1 java层泄漏

安卓中java层的内存泄漏绝大部分本质是context在组件结束后被意外持有,其中大部分是activity泄漏,因为安卓是由一个个组件构成的,一般出现内存泄漏都是Activity的context被长生命周期的对象意外持有导致无法释放Activity,进而出现Activity结束但是实际没有释放。另外也有部分是service泄漏,因为service也有context,而且可能因为实现方式不当导致没有按逻辑结束。

java层泄漏是由于context被长生命周期对象意外持有,常见的情况有以下几种:

|-----------------------------------------------------------------------------------------------------------------------------------|
| 静态对象持有Context,静态变量(如静态集合、单例)的生命周期与应用进程一致,若其持有 Activity 的引用,会阻止该 Activity 被回收。 |
| 非静态内部类/匿名类持有Context,非静态内部类(如 Handler、AsyncTask、Thread)隐式持有其外部类(通常是 Activity)的强引用。若内部类的生命周期长于 Activity(如后台线程未执行完),就会导致 Activity 泄漏 |
| 未反注册的监听器或广播,将 Activity 本身注册为监听器、广播接收器或观察者后,如果在 Activity 销毁前没有正确反注册,这些系统管理器或监听器列表会继续持有 Activity 的引用。 |
| io资源未释放,如 Cursor、File 流、Bitmap 等资源性对象使用了底层资源,如果仅置为 null而不调用关闭方法(如 close(), recycle()),其占用的内存可能无法及时释放。 |
| WebView 引用,WebView 在加载页面后会持有 Activity 的 Context 引用,如果不在 Activity 销毁时进行特殊处理,很容易造成泄漏。 |

17.2 如何排查与处理内存泄漏

java层是比较好处理的,使用Android Studio自带Profiler工具基本可以解决问题;Native层的一般要借助一些三方工具进行排查,我目前使用过比较靠谱的是字节的Raphael,可以去网上搜一下。我之前也写过关于安卓java层和native层泄漏原因、排查与解决方法的文章,可以参考一下:

Android内存泄漏排查篇1(JAVA层泄漏)-CSDN博客

Android内存泄漏排查篇2(Native层泄漏)_android native内存泄漏-CSDN博客

这里还要提到一种情况,三方库泄漏怎么办?之前也被好几家问了。首先如果是知名库或者还有人维护,直接提issue等修复,等修复完再换库;如果是小项目或者没人维护了,那就只能走点偏门手段,不解决泄漏问题,而是重启续命。大概思路就是把确定泄漏的三方库的使用逻辑都抽出来到一个新进程,结合业务来做成服务或者其它东西,通过跨进程技术进行调用,再做一套监控、杀死、重启的机制,要注意杀死重启过程对用户的友好交互,弄点加载中之类的。如果有条件的话再继续找替代方法换掉原先泄漏的三方库,解决本质问题。

18. 弱网处理

|----------------|---------------------------------------------------------------------------------|
| 1.减少数据量 | 资源使用压缩比更高的格式,像webp代替jpeg Okhttp失败时用拦截器切换参数获取较低分辨率资源,看场景 App发okhttp请求时使用gzip压缩数据 |
| 2.本地数据库缓存 | 先查数据库,没有再走网络请求,拿到数据也会存数据库 |
| 3.Webview缓存 | 但是可能有JS 状态 / 表单 / 用户状态错误等问题,需要注意。 |
| 4.分批获取数据 | 先拿必须的,后续不一定用到的等需要才拿 |
| 5.业务逻辑上进行兜底和完善 | 例如设置密码给设备,可能你发密码,但是超时没收到响应,可能是设置成功也可能失败;在重试时需要验证密码是否已经设置成功 |

如果是音视频领域,流控处理也是很重要,1.动态码率,动态降低分辨率;2.缓冲大小,根据wifi信号,4g信号强度确定缓冲时长,弱网就增大缓冲;3.丢帧策略,缓冲不足优先确保i帧,缓冲突然一下子拿到很多,做快放和丢掉非关键帧, 弱网环境可以做跳帧减少帧率;4.FEC(前向纠错):增加冗余数据用于恢复丢包;5.NACK(丢包重传):检测到丢包时请求重传。

FEC+NACK机制:

将原始数据分块(如每k个数据包为一组),通过算法生成n个冗余包(共发送k+n个包)。冗余包数量根据网络丢包率动态调整:丢包率高时增加冗余,丢包率低时减少冗余。

发送端维护发送缓冲区,存储已发送的原始数据包(用于NACK重传),缓存时间通常为RTT的2~3倍。

通过RTP序列号连续性检测丢包。若发现序列号跳跃(如收到包1、包4),短暂等待(如10ms)以区分丢包与乱序,避免误判。如果判定包2、包3丢失,尝试用FEC冗余包恢复丢包。例如,若丢失包2、包3,但收到冗余包(包5=包1⊕包2⊕包3),可通过异或运算恢复。若FEC无法恢复,接收端构造NACK报文(包含丢失包的序列号),通过RTCP反馈给发送端,从发送缓冲区提取丢失的数据包重新发送,并标记为重传包,优先级高于新数据。

19. Handler

19.1 Handler消息机制

Handler主要用于解决子线程与主线程(UI线程)之间的通信问题,基于共享内存,通过消息队列实现线程间的任务调度。

主要作用第一个是将子线程操作结果传到主线程完成ui操作,第二个是子线程完成耗时工作后通知主线程,第三个是消息队列串行执行可以规避一些多线程并发问题。

Handler有四个关键类,消息message,消息队列messageQueue,消息循环looper,消息处理器handler。1个线程只能绑定1个消息循环,一个循环对应一个消息队列,但是可以有多个处理器,一个处理器只能绑定一个消息循环。

主线程(UI线程)在启动时已自动创建Looper(通过ActivityThread.main()调用Looper.prepareMainLooper()),而子线程需要手动创建。

工作流程,首先初始化消息循环和消息队列,主线程中,系统已自动初始化Looper和MessageQueue,在子线程中需要手动调用;然后可以通过send或post的方式发消息,两种方式本质相同,post()内部会将Runnable封装为Message并设置callback字段;消息循环在消息队列取消息,如果callback不为空就执行run,否则调用handlemessage处理。

Looper通过ThreadLocal保证线程隔离,MessageQueue采用单链表结构,native层通过epoll机制实现高效等待/唤醒,Handler构造时绑定当前线程的Looper。

19.2 Handler 引起的内存泄露原因以及最佳解决方案

非静态Handler持有Activity引用可能导致内存泄漏,使用静态内部类+弱引用,onDestroy时也要移除callback和消息。因为handler发消息时,message的target字段会指向handler,最后这个消息发送到消息队列,消息队列绑定一个消息循环,主线程的消息循环通过ThreadLocalMap存储,ThreadLocalMap是静态变量。非静态内部类默认持有外部引用,所以handler持有activity引用。

19.3 子线程创建handler

先创建消息循环,Looper.prepare();然后创建Handler绑定looper;最后调用Looper.loop(); 退出时需要调用Looper.quit();

为避免手动管理 Looper,可使用 HandlerThread(已封装 Looper和消息循环):

19.4 Handler避免内存抖动的原理

通过message对象复用减少频繁创建销毁导致的内存抖动

19.5 Handler的message入队和回收

检查消息是否已绑定Handler且未被使用,按时间排序遍历找到合适插入位置,插入时使用synchronized保证线程安全,插入后如果队列处于阻塞态且插入数据需要马上执行就唤醒消息循环;

回收时在Looper.loop分发后调用msg.recycleUnchecked回收,先清理what、target、obj等字段,然后标记为未使用;加入消息池等待循环使用。

19.6 Handler为什么使用epoll不用java的wait/notify

Android 的消息循环不仅需要处理 MessageQueue中的消息,还需监听其他事件(如输入事件、传感器数据、Binder 调用等)。epoll可以同时监听多个文件描述符(FD),而 wait/notify仅能通过对象锁实现单一条件的等待,无法满足多事件监听的需求;

wait/notify依赖 JVM 的锁机制,线程唤醒涉及内核态切换和锁竞争,而 epoll_wait通过事件驱动机制唤醒,效率更高;

若使用 wait/notify实现延时消息,需通过轮询检查时间条件,导致 CPU 空转。而 epoll_wait可直接设置超时时间精确阻塞到指定时间点;

Handler的消息队列对于事件处理有优先级,例如ui绘制优先级高而且有同步屏障,epoll本身支持优先级机制。

19.7 IdleHandler

IdleHandler是 Android 消息机制中的一种轻量级任务调度工具,用于在主线程(UI 线程)空闲时执行低优先级任务,避免阻塞用户交互。IdleHandler执行时间不确定,适合低优先级任务且短时间任务。

19.8 Handler.post和view.post区别

Handler.post直接将消息投递到消息队列,view.post内部会维护一个任务队列,如果View已附着到窗口(mAttachInfo != null),直接通过 AttachInfo内部的 Handler投递任务。

若未附着,任务暂存到队列,待View.dispatchAttachedToWindow()时触发执行。执行时会将任务插入任务队列,performTraversals() 的 TraversalRunnable 优先级高, View.post() 的任务在performtraversals之后执行。

所以首次触发onMeasure时handler.post获取不到view高度,view.post可以。Handler.post时onresume还没有结束,performTraversal未加到任务队列

19.9 Looper.loop()死循环一直运行是不是特别消耗CPU资源呢?

looper有两个机制,使得只在必要时才有cpu资源消耗。

阻塞休眠机制:

当MessageQueue为空时,queue.next()方法会通过nativePollOnce()进入阻塞状态,此时主线程会释放CPU资源进入休眠,而不是空转消耗CPU。

这个阻塞机制基于Linux的pipe/epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪时才会唤醒线程。

​​精准唤醒机制:

当有新消息到达时(如用户操作或系统事件),enqueueMessage()方法会触发nativeWake()调用,通过往pipe管道写端写入数据来精准唤醒主线程。对于延时消息,系统会计算剩余时间并精确阻塞相应时长,期间完全不消耗CPU资源。

19.10 Looper.Loop()不会造成应用卡死吗

真正的卡顿是由于单个消息处理时间过长(如在onCreate/onStart/onResume中执行耗时操作),而不是Looper循环本身导致的。Looper每次只处理一个消息,完成后立即返回循环,不会累积阻塞。消息队列会根据时间戳排序,优先处理定时任务和同步屏障后的异步消息(如UI绘制)。系统对UI消息有较高的优先级,确保用户操作能得到及时响应。

20. 跨进程

20.1 AIDL

AIDL底层调用binder,是上层用于跨进程通信的机制,主要是处理多线程、多客户端并发访问的。服务端实现aidl接口,向客户端公开该接口,以便客户端进行绑定。创建 Service 并实现 onBind(),从而返回生成的 Stub 的类实例;然后需要在androidManifest文件声明服务。(通过bindservice方式启动服务)

客户端(如Activity)调用bindService() 以连接此服务时,客户端的 onServiceConnected() 回调会接收服务端的 onBind() 方法所返回的 binder 引用。

方向标签参数

in:参数由客户端设置,服务端只读

out:参数由服务端设置,客户端只读

inout:双向参数,客户端和服务端都可读写

oneway关键字:非阻塞式调用,立即返回不等待完成,任务在线程池执行,可修饰单个方法(oneway void method())或整个接口(接口内所有方法隐式成为 oneway 方法)。使用oneway修饰的方法不能有返回值,方向参数只能使用 in,不能使用 outinout标签如果客户端和服务端处于同一进程,oneway修饰符将不起作用,方法调用仍然是同步的

20.2 Messenger

底层也是调用binder,同样用于上层跨进程通信,基于handler,单线程顺序处理消息,支持基本数据类型和可序列化对象

20.3 Binder优点

一次拷贝,通过内存映射技术;发送方将数据写入内核缓存区,接收方的用户空间通过内存映射直接访问该缓存区,避免了传统IPC(如Socket、管道)的两次拷贝开销。仅需一次数据拷贝,性能优于Socket、管道(需两次拷贝),仅次于共享内存;(零拷贝但管理复杂)

Binder基于Linux的UID/PID机制进行身份验证,内核层强制校验调用方身份(UID/PID),防止非法进程访问敏感服务;

服务需注册到ServiceManager,客户端通过名称查询,避免直接暴露服务端点;

通过AIDL(Android接口定义语言)自动生成代理类,开发者无需处理底层通信细节。

20.4 Binder缺点

本地通信无此限制,跨进程传输数据大小限制为1MB-8K(用户空间),超出会触发TransactionTooLargeException,如果是异步进行传输,那数据大小限制需要再除以2。限制原因:Binder设计初衷是高频、轻量级通信,而非大数据传输。大数据使用共享内存或者contentProvider;

Binder是Android特有机制,无法直接用于非Android系统。

作用:系统服务通信,应用内多进程,跨应用功能共享。

20.5 Binder实体,binder引用,binder代理

Binder实体是真正的服务实现,处理跨进程请求,通过实现onTransact处理;维护服务状态,管理相关数据和资源;管理生命周期,响应binder驱动发出的引用计数变化和死亡通知;

Binder引用是客户端的引用对象,是服务端binder实体在客户端进程的代表,实际上做的工作是把客户端的请求转发给真正的服务对象;binder引用持有远程服务的句柄,通过handle值标识服务端的具体实例,通过transact()方法将请求发送给Binder驱动,不包含业务逻辑,仅作为通信桥梁。在Binder驱动中,每个binder引用对应一个引用结构,该结构指向内核中的对应服务端binder实体,这种引用关系由驱动维护,确保即使服务端进程重启,引用关系也能正确维护;

Binder代理是binder引用的上层封装,它为客户端提供更友好的编程接口。在AIDL生成的代码中,代理类实现了与服务端相同的接口,但内部将方法调用转换为transact()调用。

21. 架构与设计模式

需要自己去学习,结合业务如何应用。

相关推荐
xxxmine2 小时前
ConcurrentHashMap 和 Hashtable 的区别详解
java·开发语言
weixin_436525072 小时前
NestJS-TypeORM QueryBuilder 常用 SQL 写法
java·数据库·sql
oioihoii2 小时前
C++虚函数表与多重继承内存布局深度剖析
java·jvm·c++
wangchen_02 小时前
深入理解 C/C++ 强制类型转换:从“暴力”到“优雅”
java·开发语言·jvm
Wang15302 小时前
Java三大核心热点专题笔记
java
潲爺3 小时前
《Java 8-21 高频特性实战(上):5 个场景解决 50% 开发问题(附可运行代码)》
java·开发语言·笔记·学习
资生算法程序员_畅想家_剑魔3 小时前
算法-回溯-14
java·开发语言·算法
花花鱼3 小时前
android 更新后安装app REQUEST_INSTALL_PACKAGES 权限受限 + FileProvider 元数据异常
android
2501_946233893 小时前
Flutter与OpenHarmony大师详情页面实现
android·javascript·flutter