复制代码
各个平台的差别如下:
| 平台 | 说明 |
| --- | --- |
| armeabi-v7a | arm 第 7 代及以上的处理器,2011 年后的设备基本都是 |
| arm64-v8a | arm 第 8 代 64 位处理器设备 |
| armeabi | arm 第 5、6 代处理器,早期的机器都是这个平台 |
| x86 | x86 32 位平台,平板和模拟器用的多 |
| x86_64 | x86 64 位平台 |
[]( )如何减少代码资源大小
- 一个功能尽量用一个库
比如加载图片库,不要 glide 和 fresco 混用,因为功能是类似的,只是使用的方法不一样,用了多个库来做类似的事情,代码肯定就变多了。
- 混淆
混淆的话,减少了生成的 class 大小,这样积少成多,也可以从一定层度减少 apk 的大小。
- R 文件内联
通过把 R 文件里面的资源内联到代码中,从而减少 R 文件的大小。
可以使用 [shrink-r-plugin]( ) 工具来做 R 文件的内联
[]( )参考文档
[Android App包瘦身优化实践]( )
[]( )启动速度
====================================================================
[]( )启动的类型
一般分为,冷启动和热启动
冷启动:启动时,后台没有任何该应用的进程,系统需要重新创建一个进程,并结合启动参数启动该应用。
热启动:启动时,系统已经有该应用的进程(比如按 home 键临时退出该应用)下启动该应用。
[]( )如何获取启动时间
- adb 命令
adb shell am start -S -W 包名/启动类的全名
adb shell am start -S -W xxx/xxxActivity
Stopping: xxx
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=xxx/xxxActivity }
Status: ok
Activity: xxx/xxxActivity
ThisTime: 770
TotalTime: 770
WaitTime: 848
Complete
复制代码
ThisTime: 表示最后一个 Activity 启动时间
TotalTime: 表示启动过程中,所有的 Activity 的启动时间
WaitTime: 表示应用进程的创建时间 + TotalTime
一般我们关注 TotalTime
就好了。
另外,谷歌在 Android4.4(API 19)上也提供了测量方法,在 logcat 中过滤 Displayed 字段也可以看到启动时间
2021-04-06 19:25:52.803 2210-2245 I/ActivityManager: Displayed xxx/xxxActivity: +623ms
+623ms
就是Activity 的启动时间。
- 时间戳
时间戳的方法基于以下的 2 个知识点。
-
应用进程刚创建,会调用 Application 的 onCreate 方法。
- 首次进入一个 Activity 后会在 onResume() 方法后面调用 onWindowsFocusChange 方法。
结合这 2 个特性,我们可以在 A Application 的 onCreate() 方法和 Activity 的 onWindowsFocusChange 方法里面,通过时间戳来获取应用的冷启动时间。
[]( )如何监控启动过程
- systrace
systrace 是一个功能很强大的工具,除了可以查看卡顿问题,也可以用来查看应用的启动问题。使用示例如下:
python $ANDROID_HOME/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a 你的包名 -o test.log.html
用 Google 浏览器打开 test.log.html
就可以看到详细的启动信息。
- Debug 接口
package android.os;
...
class Debug {
...
public static void startMethodTracingSampling(String tracePath, int bufferSize, int intervalUs) {
}
public static void startMethodTracing(String tracePath, int bufferSize) {
}
}
复制代码
利用 Debug 类的这两个方法,可以生成一个 trace
文件,这个 trace
文件,可以直接在 AS
里面打开,可以看到从 startMethodTracingSampling
到 startMethodTracing
过程中的方法调用等信息,也可以较好的分析启动问题。
[]( )一般有那些优化方法
- 耗时操作放到异步进程
比如文件解压、读写等耗时 IO 操作可以新开一个线程来执行。
- 延时初始化
即暂时不适用的工具类等延后到使用的时候再去初始化。比如从 xml 里面读取颜色,可以考虑在使用的时候再去读取和解析。
- 线程优化
线程的创建需要消耗较多的系统系统资源,减少线程的创建。可以考虑共用一个线程池。
如何检测线程的创建,可以参考我个开源库 [performance]( )
[]( )稳定性优化
=====================================================================
[]( )APP 稳定性的维度
app 稳定一般指的是 app 能正常运行, app 不能正常运行的情况分为两大类,分别是 Crash
和 ANR
Crash:运行过程中发生的错误,是无法避免的。
ANR:应用再运行时,由于无法再规定的时间段内响应完,系统做出的一个操作。
[]( )如何治理 Crash
应用发生 Crash 是由于应用在运行时,应用产生了一个未处理的异常(就是没有被 try catch 捕获的异常)。这会导致 app 无法正常运行。
如果需要解决的话,就需要知道这个未处理的异常是在哪里产生的,一般是通过分析未处理的异常的方法调用堆栈来解决问题。
Android APP 可以分为 2 层,Java 层和 Native 层。所以如何捕获需要分开说。
[]( )Java 层获取未处理的异常的调用堆栈
这个需要了解 Java
虚拟机是如何把一个未捕获的异常报上来的。
未捕获的异常,会沿着方法的调用链依次上抛,直到 ThreadGroup
的 uncaughtException
方法
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
// 递归调用,可以忽略
parent.uncaughtException(t, e);
} else {
// 交给了 Thread.getDefaultUncaughtExceptionHandler() 来处理未捕获的异常
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
- t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
复制代码
查阅代码发现,发现 ThreadGroup
最终会给 Thread
的 defaultUncaughtExceptionHandler
处理。
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
复制代码
上面的代码显示:Thread
的 defaultUncaughtExceptionHandler
是 Thread
类的一个静态变量。
看到这里,如何捕获 Java
层未处理的异常就很清晰了,给 Thread
设置一个新的 defaultUncaughtExceptionHandler
,在这个新的 defaultUncaughtExceptionHandler
里面收集需要的信息就可以了。
需要注意的一点是 旧的 defaultUncaughtExceptionHandler
需要保存下来,然后新的 defaultUncaughtExceptionHandler
收集信息后,需要再转给旧的 defaultUncaughtExceptionHandler
继续处理。
[]( )Native 层获取未处理的异常的相关信息
Java 层如何收集未处理的异常的信息说过了,我们来看看 Native 层发生未处理的异常的话,是如何处理的。 Native 层的处理,需要掌握 linux 的一些知识,由于本人不是特别了解 linux ,这里就直接参考别人的文章了。如果有错误,还请指正。
本人通过查阅资料发现,Native 层如果发生未处理的异常(注:如果 Native 层捕获了异常,是可以通过 JNI 抛到 Java 层去处理的) ,系统会发出信号给 Native 层,在 Native 层如果要收集未处理的异常信息,就需要注册对应信号的处理函数。当发生异常的时候,Native 层会收到信息,然后通过处理器来收集信息。
注册信号处理函数如下:
#include <signal.h>
int sigaction(int signum,const struct sigaction act,struct sigaction oldact));
复制代码
-
signum:代表信号编码,可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号,如果为这两个信号定义自己的处理函数,将导致信号安装错误。
-
act:指向结构体sigaction的一个实例的指针,该实例指定了对特定信号的处理,如果设置为空,进程会执行默认处理。
- oldact:和参数act类似,只不过保存的是原来对相应信号的处理,也可设置为NULL。
有了信号处理函数,后面还要做的事情就是收集信息了,由于本人不是很熟悉 Native 的开发,这里就不展开说了了,大家可以参考 [Android 平台 Native 代码的崩溃捕获机制及实现]( )。
[]( )如何治理 ANR
ANR
是 Applicatipon No Response
的简称。如果应用卡死或者响应过慢,系统就会杀死应用。为什么要杀死应用?其实也很好理解,如果不杀死应用,大家会以为系统坏了。
那我们如何监控 ANR
呢?以及我们如何分析 ANR 的问题呢?常见的导致 ANR 的原因有哪些呢?
首先,ANR
的原理是 AMS
在 UI 操作
开始的时候,会根据 UI 操作
的类型开启一个延时任务,如果这个任务被触发了,就表示应用卡死或者响应过慢。这个任务会在 UI 操作
结束的时候被移除。
然后,如何分析 ANR
问题呢?
一般 ANR
发生的时候, logcat
里面会打印 ANR
相关的信息,过滤关键字 ANR
就可以看到,这里不做详细分析,可以参考后面的文章。
然后一般会在 /data/anr
目录下面生成 traces.txt
文件,里面一般包含了 ANR
发生的时候,系统和所有应用的线程等信息(需要注意的是,不同的 rom 可能都不一样),通过 logcat
打印的信息和 traces.txt
里面的信息,大部分的 ANR
可以分析出原因,但是呢,也有相当一部分的 ANR 问题无法分析,因为 logcat
和 traces.txt
提供的信息有限,有时候甚至没有特别有用的信息,特别是 Android
的权限收紧, traces.txt
文件在高 Android 版本
无法读取,给 ANR
问题的分析增加了不少的困难。不过好在最近发现头条给 ANR
写了一个系列的文章,里面对 ANR 问题的治理方法,个人觉得很好,这里引用一下。
-
[今日头条 ANR 优化实践系列 - 设计原理及影响因素]( )
-
[今日头条 ANR 优化实践系列 - 监控工具与分析思路]( )
-
[今日头条 ANR 优化实践系列分享 - 实例剖析集锦]( )
- [今日头条 ANR 优化实践系列 - Barrier 导致主线程假死]( )
本人之前写过一个小的[性能监测的工具]( ),其中有监控 UI
线程 Block
的功能,考虑后续加入头条的 ANR
监测机制,等后续完成了,在做一个详细的总结吧。这次的总结就写到这里。
[]( )内存的优化
=====================================================================
硬件的内存总是有限的,所有每个应用分到的内存也是有限的,所有内存的优化很有必要,否则应用
就没有足够的内存使用了,这个时候就会 Crash 。
[]( )内存都消耗在哪里了
优化内存的话,需要了解内存在哪里消耗了了,针对内存消耗大的场景做优化,对症下药,才可以有一个好的优化效果。
Android Studio
里面的 Profiler
工具是一个很好用的工具,通过里面的 memory
工具可以实时监控
APP 运行过程中的内存分配。
dump APP 内存堆栈后,还可以看到各个类占用的内存情况。
可以查看每个对象的详细信息。
Android Studio
里面的 Profiler
工具的具体使用教程请参考[官方教程]( ),这里就不做详细介绍了。
[]( )如何合理使用内存
利用上面的方法,找到内存消耗大的场景,就需要做优化了,主要做法就是想办法减少特定场景下的内存的使用。个人总结了一下平时可能会做的优化。
- 图片相关的优化
图片是我目前做的应用里面占用内存比较大的一块了,也碰到了一些问题,我主要是通过以下的方法来做优化。
-
暂时用不上的图片不加载,比如说,有个网络加载异常的图,不要一开始就初始化,等到真的有异常了需要展示的时候再初始化
-
加载图片的时候,尽量加载指定大小的图片,因为有时候会碰到控件的大小小于实际图片尺寸的情况,这个时候,会浪费一些内存。有需要的话,可以让后台返回不同尺寸的图片。
-
根据不同的图片格式
- 不显示的图片,可以考虑先释放。
- 尽可能少地创建对象
毫无疑问,如果对象少,内存肯定也消耗的少,那平时需要注意哪些呢?
-
自定义 view 的时候,不要在 onDraw 方法里面频繁创建对象。因为 onDraw 方法可能会频繁调用,这个时候就会创建大量的对象。从而造成浪费,同时也会导致 gc 触发的频率升高,造成卡顿。
-
尽量少创建线程,创建线程其实是比较消耗资源的,创建一个空的线程,大概会占用 1-2 M 内存。同时一般异步任务很快就会执行完,如果频繁创建线程来做异步任务,除了内存使用的多,还可能 GC 造成卡顿。执行异步任务的话,一般建议用线程池来执行,但是需要注意线程池的使用。
- 尽量用 StringBuilder 或者 StringBuffer 来拼接字符串。平时发现的问题主要是在打印 logcat 的时候和拼接后台返回的数据的时候会创建大量的 String,所以如果有类似的情况也可以考虑做一些优化。
[]( )内存泄漏是什么
内存泄漏指的是本应该释放的内存,由于一些原因,被 GC ROOT 对象持有,从而而无法在 GC 的时候释放,这样可能会导致的一个问题就是,重复操作以后,APP 没有足有的内存使用了,这个时候系统会杀死 APP 。所以内存泄漏是需要排查的。
[]( )如何监控和分析内存泄漏问题
上一个小结总结了上面是内存泄漏,是因为某些 GC ROOT 对象持有了期望释放的对象,导致期望释放的内存无法及时释放。所以如何监控和分析内存泄漏问题就成了如何找到 GC ROOT 的问题。
一般手动分析的步骤是:重复操作怀疑有内存泄漏的场景,然后触发几次 GC 。等几秒钟后,把 APP 的内存堆栈 dump 下来(可以使用 as 的工具 dump),然后用 sdk 里面的 cover 工具转换一下,然后用 MAT 工具来分析内存泄漏的对象到 GC ROOT 的引用链。
手动分析总是很麻烦的,一个好消息是,有一个特别好用的自动监控和分析内存泄漏的工具,这个工具就是 leakcanary ,它可以自动监控并给出内存泄漏的对象到 GC ROOT 的引用链。
使用很简单,只需要在 APP 的 build.gradle 下面新增
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
复制代码
leakcanary 比较核心的一个原理就是利用了弱引用的一个特性,这个特性就是:
在创建弱引用的时候,可以指定一个 RefrenceQueue ,当弱引用引用的对象的可达性发生变化的时候,系统会把这个弱引用引用的对象放到之前指定的 RefrenceQueue 中等待处理。
所以 GC 后,引用对象仍然没有出现在 RefrenceQueue 的时候,说明可能发生了内存泄漏,这个时候 leakcanary 就会 dump 应用的 heap ,然后用 shark 库分析 heap ,找出一个到 GC ROOT 的最短引用链并提示。
[]( )常见的内存泄漏的场景
个人总结了下工作中碰到内存泄漏的一些场景,现记录下来,大家可以参考下。
最后
一个零基础的新人,我认为坚持是最最重要的。我的很多朋友都找我来学习过,我也很用心的教他们,可是不到一个月就坚持不下来了。我认为他们坚持不下来有两点主要原因:
他们打算入行不是因为兴趣,而是因为所谓的IT行业工资高,或者说完全对未来没有任何规划。
刚开始学的时候确实很枯燥,这确实对你是个考验,所以说坚持下来也很不容易,但是如果你有兴趣就不会认为这是累,不会认为这很枯燥,总之还是贵在坚持。
技术提升遇到瓶颈了?缺高级Android进阶视频学习提升自己吗?还有大量大厂面试题为你面试做准备!
提升自己去挑战一下BAT面试难关吧
对于很多Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些知识图谱希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
不论遇到什么困难,都不应该成为我们放弃的理由!
如果有什么疑问的可以直接私我,我尽自己最大力量帮助你!
最后祝各位新人都能坚持下来,学有所成。