# AndroidLearning **Repository Path**: luo_jun99/android-learning ## Basic Information - **Project Name**: AndroidLearning - **Description**: 学习安卓开发 - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-08-13 - **Last Updated**: 2025-04-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # AndroidLearning # Gradle 不支持 socks5代理,所以Android Studio 一律只能使用http代理 ## 环境信息 ### AOSP android-13.0.0_r44 ### Ubuntu 24.04 # 零、vs code使用 clangd 跳转查看Android源码 ```shell cd ~/android/android source build/envsetup.sh lunch rk3566_tspi-userdebug # 启用编译数据库生成 export SOONG_GEN_COMPDB=1 export SOONG_GEN_COMPDB_DEBUG=1 # 运行构建(可以指定特定模块加速生成) build/soong/soong_ui.bash --make-mode nothing -j16 # 生成的数据库位于 ls out/soong/development/ide/compdb/ # 为 compile_commands.json 创建软连接 ln -s out/soong/development/ide/compdb/compile_commands.json . ``` ## vscode 安装 clangd 插件 ```shell sudo apt install -y clangd-18 ``` ```shell luo@ubuntu ~/android_13> repo info Manifest branch: refs/tags/android-13.0.0_r44 Manifest merge branch: refs/heads/android-13.0.0_r44 Manifest groups: default,platform-linux # 初始化环境变量 source build/envsetup.sh # 选择目标平台(只能选这个,其他因为未知原因会报错,当前的我无法解决) lunch sdk_phone_x86_64 # 编译 make # 运行模拟器 emulator ``` #### 官方文档 [Activity生命周期](https://developer.android.com/codelabs/android-fundamentals-02-2-activity-lifecycle-and-state?hl=zh-cn#0) #### 动态 Fragment 优点 * 把本应该是 PagerAdapter 来实例化的 layout.xml,单独委派给了 Fragment 子对象 来进行实例化 * Fragment 有自己完整的生命周期,在比较复杂的页面,把页面拆分成多个Fragment页面而不是放在一个Activity里面, 可以降低耦合 * 如果有超多Fragment,那么系统会自动管理其生命周期 * 当一个页面长时间不用时,系统会自动将其销毁,减少资源消耗, 一旦发生页面切换,只有相邻的页面会被加载,非相邻页面被回收 * 在 Fragment 中可以访问 Context,Fragment至此已经和 Activity 没有太大差别了 #### Gradle 配置代理 > ~/.gradle/gradle.properties 下 ```properties systemProp.http.proxyHost=localhost systemProp.http.proxyPassword=1234 systemProp.http.proxyPort=20809 systemProp.http.proxyUser=luo systemProp.https.proxyHost=localhost systemProp.https.proxyPassword=1234 systemProp.https.proxyPort=20809 systemProp.https.proxyUser=luo ``` # 一、Android卡顿原因 CPU/GPU刷新帧缓冲时间 小于 屏幕刷新率 画面撕裂,使用双缓冲 渲染一帧的时间超过16ms 没有帧缓冲可以使用,加大帧缓冲区的数量 SurfaceFlinger 等待CPU/GPU 处理帧 导致 屏幕显示上一帧画面,而没有刷新屏幕 Choreographer(编舞者)接收来自 SurfaceFlinger 的垂直同步信号刷新信息 然后向应用程序主线程发送一个消息,来触发画面刷新 但是如果主线程因为以下原因没有及时更新,就会造成卡顿 0、view 层级过度绘制 1、内存原因 jvm stw 内存抖动 2、cpu等待时间片 3、IO阻塞主线程 4、使用了锁 1、发现问题 app使用卡顿(跳帧) 2、定位问题 工具应用 systrace # 二、应用程序崩溃 1、java 层崩溃 Zygote 启动应用程序是在RuntimeInit.CommonInit() 中为每一个进程设置了全局的线程异常处理器 `Thread.UncaughtExceptionHandler ` 线程抛出异常时,异常处理器会将其发送给AMS AMS发送给 DropBoxManagerService DropBoxManagerService 会把异常信息写入到 `/data/system/dropbox` 2、Native 层崩溃 AMS 的 startObservingNativeCrashes() 启动一个线程通过socket监听 `/data/system/ndebugsocket` 接收到异常信息之后也会 通过`DropBoxManagerService` 写入到 `/data/system/dropbox` 3、操作超时 以启动Service为例 依赖于Looper延迟消息超时未被处理 AMS.MainHandler 处理 SERVICE_TIMEOUT_MSG 消息 用户点击屏幕时,IMS会发送ANR消息,如果没有被及时处理,就会发生ANR ## 面试技巧:如何优化? 基础: 内存、线程、IO/网络、算法 framework: 通信机制 系统进程之间的协作问题 启动流程 渲染机制 1、发现问题(讲原理) 系统为什么会崩溃 系统为什么会卡顿 2、定位问题 通过原理去判断当前属于哪种类型 3、解决问题 基础+framework的理解 规避掉引起卡顿或者崩溃场景的问题 # 三、ANR 问题解决思路 1、查看ANR 时间信息 可以去主日志文件log.txt前后10秒查看相关信息 2、看主线程状态 Runnable: 可以排除 死锁 wait 原因 可能是主线程出问题了 注意:其他线程抛出异常未被处理也会导致ANR 3、根据主线程调用堆栈信息 判断问题类型 看到有 事件相关的 android.view.InputEventReceiver 在栈中查看问题的事故点(在哪一个方法中抛出的异常) 得出结论是 输入事件没有得到及时处理,导致的ANR 4、根据 log.txt 判断是不是CPU满载 不是CPU满载可能是内存原因 5、ANR 日志判断是否是内存问题 高频GC 导致 事件未被及时处理 => ANR 所以当前事件里可能有高频对象生产 RecycleList 没有被合理使用 里面有图片资源 大量生产 ## 日志文件 ANR日志: 内存状态、各个线程状态(主要看主线程) log.txt 主日志文件:CPU的使用判定 # 四、如何降低 崩溃几率 > 不要让整个程序被这么不友好的 killed 掉 ## 1、重启主线程的 looper * 1、替换主线程的 UncaughtExceptionHandler * 2、收集并上报数据 * 3、再次调用Looper.loop() 来恢复主线程的执行 ## 2、报错时只退出当前activity,对 ActivityThread.mH 拦截其 handleMessage() 抛出的异常 ## 3、应用重启 # 五、内存优化 ## 大量创建对象,自定义view 实现loading的加载动画 多次创建 new Path() 、new Paint() 对象-> 创建 Path对象的操作不要放在 onDraw() 中,而是应该在构造器中进行初始化 当需要多个对象时,就 path.reset() 之后就能继续使用 onDraw() 中多次使用 Color.parseColor("#1188ee") 其中调用了解析字符串的函数 ## 查找内存泄漏的方法 在起始页面dump内存,进行各种操作,页面跳转,完成之后再回到起始页面,再dump内存,把两者进行比较 使用`Android/Sdk/platform-tools/hprof-conv`工具转换一下 再使用MAT(Memory Analyzer tools)来分析 比较两者内存 `右键 -> Merge shotest path to GC root -> exclude all weak references` 只看强引用 原因:在自定义 View 中的 onAttachedToWindow() 中启动了一个动画 但是并没有将其释放,因为动画的 ValueAnimator.addUpdateListener() 方法设置了一个匿名内部类 这个匿名内部类一直持有View对象,导致View对象不能被回收 解决方法: 在 onDetachedFromWindow() 将动画资源释放 将其改写为静态内部类并不能完美解决问题,因为 View 对象是释放了,可以 UpdateListener 本身还是内存泄漏了 > 编程经验: > 提升自己写代码的能力 > * 枚举是一个对象、比较消耗内存,AOSP 中基本没看到什么枚举 > * 所有基本数据类型的成员可以写成 static final > * 注意字符串拼接 > * 重复申请内存 > * 不要在 onMeasure() onLayout() onDraw() 中调用 requestLayout() > * 避免重复频繁创建对象:避免GC回收将来会用到的对象 > * Activity 内存泄漏 > * 非业务不要把Activity的上下文作为参数传递,可以传递Application的上下文 > * 与Activity关联的组建不要写成 static,例如 private static Button, private static Drawable > * 非静态内部类会持有外部类,建议单独写一个类 > * 单例模式持有 Activity 引用 > * handler.postDelay() * 如果开启的线程需要传入参数,用弱引用接收可以解决问题 * handler 记得清除 removeCallbackAndMessages() * 尽量使用 IntentService 而不是 Service * onResume() 中创建的资源、需要成对的在onStop()方法中将其释放 * 禁止在View、Activity中使用非静态内部类 ## java内存优化方案 ### 减少加载进程java堆的内存 1、评估`LruCache`所涉及业务的具体内存占用和用户机型来决定`LruCache`的大小,而不是全部设置为最大可用堆内存的1/8 2、按需加载数据(懒加载) linux mmap就是懒加载(到具体使用时才分配虚拟内存) 3、转移数据到Native层 借助`Native Hook`技术(BitMap中有使用参考),把java堆中的数据转移到Native层中 4、把例如小程序、Flutter、RN、WebView等放入子进程中 ### 及时清理加载进java堆的内存 > 把不再使用的对象对于GC Root的引用给切断 > 什么时候切断引用呢? > * 业务结束时 > * 内存不足时 > 好用的内存泄漏诊断工具 [LeakCanary 🐤](https://github.com/square/leakcanary) #### 1、业务结束时常犯的错误 `Activity`执行`onDestroy()`之后,其`Context`依然被其他对象使用 解决方法 * 传递`Activity.getApplicationContext()`而不是传递`Activity`对象本身 * 传递`Activity`的弱引用对象 * 在`Activity`成套的生命周期方法中手动释放资源 #### 2、内存不足时,切断非必要对象与`GC Root`的联系 > 如何知道java堆内存不足呢? > 开启子线程,一段时间检测一次 > 向Handler循环发送延迟消息来清理检测 ```java //获取当前虚拟机实例的内存使用上限 Runtime.getRuntime().maxMemory() //获取当前已经申请的内存 Runtime.getRuntime().totalMemory() ``` ### 增加java堆的可用大小 ## Native内存优化 > Native 异常占用大量内存原因 * so库中申请了非常大量内存 * so库有内存泄漏 解决方案 * 修改so库及时free内存 * 更换稳定版本的so库 ### BitMap 优化 > 不建议使用[Lancet](https://github.com/eleme/lancet) AOP 框架,把下述逻辑封装为切面,因为已经很久没有维护了 #### 1、对超过阈值的图片进行按比例缩放,比如说宽度缩小为屏幕宽度 * 质量压缩 * 采样率压缩 * 缩放压缩等 * 采样率压缩 重设图片的采样率,降低图片像素 设置采样率 * options.inSampleSize = 5; * 尺寸压缩 减少单位尺寸的像素值,真正意义上的降低像素 * 使用矩阵 大图小用用采样,小图大用用矩阵 * 设置矩阵:matrix.setScale(2, 2, 0f, 0f); * canvas.concat(matrix); * 图片格式 不同图片格式质量不一样 采用适当的图片格式(PNG,JPEG,WEBP,.9图) * 图片像素格式 不同图片的解码方式对应的内存占用大小也会有差异 采用适当的像素格式(ARGB_8888,RGB_,565) * 使用图片缓存库 Glide和Picasso作为优秀的图片开源库,做了相应的图片优化处理 * Glide:更快,缓存策略更好,支持GIF * Picasso:图片质量更高,不支持GIF #### 2、图片资源压缩 [压缩图片](https://tinypng.com/) #### 3、把ARGB格式修改为 RGB565 内存减小为原来一半 > 既能减少存储空间,也能减少内存占用 ```java /** * 将ARGB格式转换为RGB565格式。 * * @param argb 32位的ARGB整数,其中A是Alpha通道(8位),R是红色通道(8位),G是绿色通道(8位),B是蓝色通道(8位)。 * @return 16位的RGB565整数,其中R占5位,G占6位,B占5位。 */ public static int argbToRgb565(int argb) { // 提取ARGB的各个通道 int a = (argb >> 24) & 0xFF; // Alpha通道(忽略) int r = (argb >> 16) & 0xFF; // Red通道 int g = (argb >> 8) & 0xFF; // Green通道 int b = argb & 0xFF; // Blue通道 // 将R从8位压缩到5位 int r5 = r >> 3; // 将G从8位压缩到6位 int g6 = g >> 2; // 组合RGB565的值 int rgb565 = (r5 << 11) | (g6 << 5) | b; return rgb565; } ``` ```java byte data[] = new byte[100*100*2]; for(int i=0;i `new Thread()` 最终调用`pthread_create()` 然后调用`clone()` > 一个线程占用`1MB`的虚拟内存 ### 1、减少线程的数量 * CPU密集型线程池:线程数量为核心数 * IO密集型,核心线程数 根据业务调整为0~3个线程,最大设置为64个 ### 2、收敛野线程,线程池 > 小项目直接手动从`new Thread()`用idea查找引用来修改 > 大项目使用aop切面修改 ### 3、修改线程栈大小为 512kb ```c static size_t FixStackSize(size_t stack_size) { if (stack_size == 0) { stack_size = Runtime::Current()->GetDefaultStackSize(); } stack_size += 1 * MB; …… return stack_size; } ``` ![](./images/new_thread_stack_size_512kb.awebp) 当我们将应用中线程栈的大小全改成 512 kb 后,可能会导致一些任务比较重的线程出现栈溢出,此时我们可以通过埋点收集会栈溢出的线程, 不修改这部分线程的大小即可。总的来说,这是一个容易落地且投入产出比高的方案。 ### 4、多进程架构优化 > 面试可以说把web页面单独放到子进程中 建议放到独立进程中 * WebView 相关的业务 * 小程序相关的业务 * Flutter 相关的业务 * RN 相关的业务 并且,将这些业务放在子进程中也很简单,只需要在承载这些业务的 activity 的 mainfest 配置文件中添加 android:process = "子进程名" 即可。 需要注意的是,如果我们把业务放在子进程,就没法直接和主进程通信了,需要借助 Binder 跨进程通信的方式来完成。 ## 任务调度优化 在 Linux 系统中,任务调度的维度是进程,Java 线程也属于轻量级的进程,所以线程也是遵循 Linux 系统的任务调度规则的,那进程的调度规则又是怎样的呢?Linux 系统将进程分为了实时进程和普通进程这两类,实时进程需要响应技术的进程,比如 UI 交互进程,而普通进程对响应速度要求不是非常高,比如读写文件、下载等进程。两种类型的进程的调度规则也不一样 主要介绍 SCHED_FIFO 。当系统使用先进先出的策略来调度进程时,如果某个进程占有 CPU 时间片,此时没有更高优先级的实时进程抢占 CPU,或该进程主动让出,那么该进程就始终保持使用 CPU 的状态。这种策略会提高进程运行的持续时间,减少被打断或被切换的次数,所以响应更及时。Android 中的 AudIO、SurfaceFlinger、Zygote 等系统核心进程都是实时进程。 ### 线程优化 #### 1、收敛应用中的线程,包括野线程和各个业务的自定义线程池等 #### 2、使用线程池, 我们在应用开发过程中使用的线程,最好全部都是从线程池创建的,并且我们还要能正确地使用线程池 > 根据任务的类型来进行调度。如果是 CPU 类型的任务,就需要放在 CPU 线程池中去运行,如果是 IO 类型任务,就需要放在 IO 线程池去运行 。那 如果我们对所运行的任务类型不清楚怎么办?我们可以通过插桩将 Runnable 的 run 方法的执行时间以及对应的线程池打印出来,如果任务耗时较久, 还 是在 CPU 线程池执行的,那我们就需要考虑该任务是否需要放在 IO 线程池去执行了。 #### 3、充分利用 CPU 闲置时刻 为什么预加载任务要放在 CPU 的闲置时刻呢?如果预加载任务不是放在 CPU 的闲置时刻就会和核心场景抢占资源,导致核心场景速度变慢。比如,我们经常会在启动时预加载一些逻辑以此来提升后面场景的速度,但这样会导致启动变慢。如果把这些任务放在 CPU 闲置后再执行,就能做到既不影响启动的速度,又能提升后面场景的速度了。 检测到 CPU 已经闲置,我在这里介绍两种方案: ##### (1) 通过读取 proc 文件节点下的 CPU 数据判断 CPU 是否闲置; ##### (2) 通过 times 函数判断 cpu 是否闲置。 ### 4、那么,怎样才能减少等待 IO 导致的 CPU 使用率下降呢 #### IO 任务分离 > 将 IO 的任务从主线程或者主流程中分离出来,单独用 IO 线程池去处理 > 当 IO 线程将这个 IO 任务处理完成,再通知主流程拿处理完成的结果进行接下来的逻辑。 > 主线程必须要先拿到 IO 任务的结果,才能进行后面逻辑的话,主线程不还是需要等待 IO 吗? > > 实际上,IO 任务分离并不能做到不等待 IO,但是可以缩短我们等待 IO 的时间, > 只要你将主流程中的任务拆分得够细,一定有可以先执行的任务, > 比如我想渲染某个界面,需要 IO 任务读取界面的展示数据。这个时候,如果我们可以先将界面创建并渲染出来, > 然后用默认静态数据替代,等 IO 拿到最新数据后再进行界面的更新,那主线程等待 IO 的时间就缩短了很多。 ##### 解决方案 ###### 1、使用AsyncTask对象 > 但是对于`GridView`,`ListView`,`RecyclerView`,`ViewPager2`,如果每一个列表项都包含一张待解析的大图 > `AsyncTask` 一次只能解析一项,不能重复`execute()` > 不好使 ##### 2、 使用Handler > 可以自定义一个 Handler ,数据加载完成之后,向这个handler发送一个更新UI的消息,让主线程来执行 > 但是要注意因此而产生的Handler Activity 内存泄漏 #### 使用协程 > Kotlin 的协程不受内存调度器的限制,Kotlin 的协程不能理解为线程。 > 实际上,当你创建协程时,这些协程实际都在同一个进程上运行, > Kotlin 内部实现了调度机制,就像内存的进程调度机制一样,去调度执行这个协程任务。 > > 所以通过协程任务,我们就能在等待 IO 的时候去执行其他任务,并且进程也不会休眠,而是一直运行的状态。 > 实际上,灵活的使用协程对于 io 密集型应用来说帮助是很大的,会让我们程序性能表现的更好。 > > 个人认为:在kotlin协程上并发使用CPU密集型任务并不能带来收益,甚至协程切换开销大于所带来的收益 > 但实际上,io任务分离在java环境下实现比较啰嗦,不如kotlin协程方便 ![](./images/optimize_cpu.awebp) ### 提升缓存命中率 冷热端分离的 LruCache 最近最少被使用的图片就会被淘汰 我们可以想象这样一个场景,一款聊天类的 App 运行在一台内存容量较低的低端设备上,此时该应用 LruCache 的容量很小,假如只能放 10 张图片。这款聊天应用的核心场景是在会话页中,如果此时在会话页中打开一篇公众号,这个公众号的图片又比较多,很快就会达到 LruCache 的容量上限,于是 LruCache 便把缓存的 10 张和会话页相关的图片都淘汰了。可这篇公众号我们实际上看过一遍就不会再打开了,会话页却会被反复打开,所以这时使用 LruCache 策略就会导致缓存的命中率很低,会话页的页面展示速度自然也就慢了很多。 因此,在缓存容量较低的场景下,LruCache 的表现其实并不好,因为最近最少被使用的这张图片并不代表这张图片不是被频繁使用的图片。 对于上面的这个场景,我们就可以根据使用频率来淘汰。 ### Dex 类文件重排序 > 这一优化方案实际是从操作系统和硬件层的角度去思考的,程序运行实际上是 CPU 在不断读取程序指令并执行的过程。CPU 在读取指令时,会先从寄存器读,寄存器没有再从高速缓存读,最后才从主存读,读取到指令后,也会先从主存加载到高速缓存,再从高速缓存加载到寄存器。高速缓存从主存读取的数据量的大小是有限的,这个大小为 cache line 个字节,高速缓存实际上也是被分为了一个个cache line 大小的块。cache line 的大小和 CPU 型号有关,主流的是 64 个字节。 ### 需要调整优先级的线程 从Android5 开始,主线程只负责布局文件的 measure 和 layout 工作,渲染的工作放到了渲染ls线程,这两个线程配合工作,才让我们应用的界面能正常显示出来。所以通过提升这两个线程的优先级,便能让这两个线程获得更多的 CPU 时间,页面显示的速度自然也就更快了。 主线程的优先级好调整,我们直接在 Application 的 attach 生命周期中,调用 Process.setThreadPriority(-19),将主线程设置为最高级别的优先级即可 ### 核心绑定 CPU 大核 线程绑核并不是很复杂的事情,因为 Linux 系统有提供相应的 API 接口,系统提供的 pthread_setaffinity_np 和 sched_setaffinity 这里两个函数,都能实现线程绑核。但是在 Android 系统中,限制了 pthread_setaffinity_np 函数的使用,所以我们只能通过 sched_setaffinity 函数来进行绑核操作。 # 六、启动优化 ## Activity 启动时间 * Logcat 上面 过滤标签 Displayed 就可以看到 ## 每个方法的启动时间 ### 1、在方法中调用 Debug.startMethodTracing() Debug.stopMethodTracing() ### 2、使用 Android Studio Profile,勾选 start this recording on startup ![](./images/as_profile.png) ### 3、abd shell am start -W ## 1、黑白屏问题 图片、动画 ## 2、setContentView() * 处理过度绘制 * 减少布局中ViewGroup层级 * 尽量不要在Application和Activity的onCreate() 中写初始化代码 > 应该在 Activity.onWindowFocusChanged() 中进行欢迎页的业务初始化 > 这样能确保初始化业务逻辑之前,用户界面已经成功渲染 > 使用 启动框架、懒加载、预加载 > Debug.startMethodTracing() 更多参考阿里android开发规范 # 七、APK 优化 ## 资源文件优化 * assets 目录文件 :res/raw 以及 assets 目录中的文件都是源文件,我们在工程代码中该目录下所放的文件都直接会被打进 apk 包中 * res 资源文件:这些资源中,除了 raw 目录下的文件以及 png、jpg、gif 等图片文件,其他的 xml 文件都已经被编译成二进制格式。 * resources.ars 文件:resources.arsc 文件存放的是 res 目录下文件类型资源的索引,以及非文件类型资源的值。 上面这三类文件,我们通常可以采用精简、压缩、动态化这三条方法论来进行体积优化。 * 精简:精简就是减少文件的大小或者优化文件中数据段里数据。比如针对 dex 文件,我们可以通过减少类文件,又或者移除 dex 文件中 data 数据区中的 debug 信息等方案来进行精简优化。再比如针对 so 文件,我们可以减少不必要的 so 引入,又或者移除 so 文件中符号表这一数据段等方案进行精简优化。 * 压缩:只要是文件能就能进行压缩,只要有压缩,我们就可以尝试替换更优的压缩算法。 dex、so、资源文件都可以被压缩,比如我们可以将 dex 文件压缩,然后在使用时接管系统对 dex 文件的解压来实现这个方案,so 文件同样适用。 * 动态化:插件或者网络拉取都属于动态化的方案。比如 dex 、so、res 资源,我们都可以做成插件的形式进行下发,图片我们也可以尽量通过网络来下载。 ### 无用资源检测和删除 #### 开启 shrinkResources 来扫描项目中没有被使用到的资源,并进行删除。 #### 在 gradle 配置中开启 shrinkResources 此外,我们还需要注意在 proguard 文件中关闭 dontshrink 字段,否则扫描出的无用资源并不会被优化。 ## 布局优化 > [文章参考](https://blog.csdn.net/m0_37796683/article/details/102590141?spm=1001.2014.3001.5506) * FrameLayout > LinearLayout -> RelativeLayout * 使用 标签提升布局复用性 * 使用 标签减少 其他布局的时候的嵌套层次 * 使用 减少初次测量/绘制的时间 ## 绘制优化 * 不要在`onDraw()`方法中频繁创建对象 * 避免`onDraw()`方法中的耗时操作 ### 避免过度绘制 > 手机设置--开发者选项--调试GPU过渡绘制--显示过渡绘制区域 * 移除默认的windows背景 * 移除子控件中不必要的背景,与父控件背景颜色相同的子控件,不要单独为子控件额外设置背景 * 自定义控件View优化:使用clipRect() 、 quickReject()