一个质量较高的性能分析专栏
我们先根据这个专栏出发,验证其各种方案的可行性。
性能优化大纲
1.性能优化目的
性能优化的目的是:提高应用流畅性、稳定性、节省资源。
①流畅性:运行的更加流畅、不卡顿
②稳定性:稳定运行,使用过程中不出现引用崩溃和无响应(ANR)问题
③节省资源:节省耗费的资源,包括安装包大小、内存占用、耗电量、网络资源(手机流量)等
2.性能指标
流畅性、稳定性、资源节省性
具体操作
优化操作一:流畅性
主要针对三个方向优化:
启动速度、页面显示速度、响应速度
优化方案:
采用异步加载、分部加载、延期加载策略,减少主线程中的操作时间。
具体操作:
1.启动速度
1.减少onCreate的时间。
可以将onCreate的代码放到onResume中执行,不过为了防止重复初始化,需要加标志位。
2.优化布局文件
优化布局层次结构:使用include、merge、ViewStub、ConstraintLayout等。
merge使用主要是的目的是在优化布局,减少布局嵌套,一般与include结合使用
观察布局的工具:Hierarchy Viewer
优化布局的工具:Layoutopt
3.提高Adapter、ViewHolder的效率
4.减少主线程的耗时时间
将耗时过长的操作放入后台线程中执行,只在需要修改UI时通知主线程修改。
2.页面显示速度
优化方案:布局优化 、绘制优化
Android性能优化:手把手带你全面了解 绘制优化
Android性能优化:布局优化 详细解析
主要优化方向:
①降低View.onDraw的复杂度
②避免过度绘制(Overdraw)
具体操作就是:
第一点:不要在我们自定义的View中创建新的局部变量。
避免onDraw中执行大量或者耗时的操作。
第二点:移除默认的Window背景、移除控件中不必要的背景、减少布局文件的嵌套、自定义空间View优化(使用#clipRect、#quickReject)
可以在开发者选项中打开<调试GPU过度绘制>开关,比如蓝色是一层、绿色两层、红色四层等,尽量在两层以内,也就是绿色。(微信整个是红色的...)
第二点实际操作:
①移除默认的Window背景:
在应用程序默认继承的主题中,也就是清单文件中application项中的theme中:
添加一行:
<item name="android:windowBackground">@null</item>
也就是设置windowBackground为null。
②③可以使用AS自带的Layout Inspector来查看页面布局嵌套情况。(Hierarchy View已被废弃)
④自定义控件View优化:使用clipRect()/quickReject()
Canvas#clipRect:
给Canvas设置一个裁剪区域,只有该区域内才会被绘制,区域之外的都不绘制。
比如抽屉布局可以使用这个。
quickReject:
判断和某个矩形相交,若判断与矩形相交,则可跳过相交的区域,减少过渡绘制。
其他优化方案:
使用OpenGL绘图。(Android最高级的绘图机制,没用过..)
Profile GPU Rendering工具:开发者选项中可打开此开关,用于在屏幕上实时显示GPU渲染每一帧图像花费的时间。
参考资料
总结:
3.响应速度
优化原因:
应用程序出现ANR情况,从而导致应用程序响应速度慢。
ANR原因
①应用在5s内未响应用户的输入事件
②广播接收器在10s内未完成相关的逻辑
③Service在20s内无法处理完成(不是15s哦)
【检测主线程中是否有耗时行为】
【开启StrictMode模式】
在Application的onCreate中开启:
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
然后过滤信息:
`adb logcat | grep StrictMode`
StrictMode详解
博客详解
StrictMode可以用于捕获应用主线程中发生的磁盘I/O、网络访问违规等问题。(并不会检测到耗时任务,比如Thread.sleep)
主要检测两大问题:线程策略(ThreadPolicy)和VM策略(VmPolicy)。
ThreadPolicy线程策略:
1.自定义的耗时调用:使用detectCustomSlowCalls()开启(检测不到耗时操作,需要自己定义)
2.磁盘读取、写入(I/O操作):detectDiskReads()、detectDiskWrites()
3.网络操作:detectNetwork()
VmPolicy虚拟机策略:
1.Activity泄露,使用detectActivityLeaks()开启(都不用LeakCanary)
2.未关闭的Closable对象泄露,使用detectLeadekClosedObjects()开启;
3.泄露的Sqlite对象,对象detectLeakedSqliteObjects
4.网络操作,使用detectNetwork
过滤日志:StrictMode
优化方案:
使用多线程,实际开发中,当一个进程发生了ANR后,系统会在/data/anr目录下创建一个traces.txt文件,通过该文件可定位出ANR的原因。
总结:
4.稳定性
大神博客
主要是:应用崩溃(Crash)、应用无响应(ANR)
ANR上面已经说了,主要看应用崩溃。
应用崩溃Crash很多情况是因为内存溢出,即OOM,故需要避免OOM现象。
因为内存优化很重要,所以我们来
内存优化
1.Android内存管理机制
从上图可以看到,Android内存分配主要就两个工作:
内存分配+内存回收(释放)
内存管理的对象:①进程②对象③变量
Android系统分为3个层次:
Application Framework、ART虚拟机、Linux内核
其中,负责进程内存的角色:Linux内核 、Application Framework
负责对象、变量内存的角色:ART虚拟机
针对进程的内存策略
由ActivityManagerService集中管理所有进程的内存分配。
内存回收策略,分两步:
1.由Application Framework决定回收的进程类型
Android中的进程是托管的;有进程空间紧张时,会按进程优先级从低到高的顺序自动回收进程。
Android进程分为5个优先级:
前台进程、可见进程、服务进程、后台进程、空进程
当系统需回收进程时,最先回收空进程、最后回收前台进程
2.Linux内核真正回收具体进程
1.ActivityManagerService对所有进程进行评分(评分存放在变量adj中)
2.更新评分到Linux内核
3.由Linux内核完成真正的内存回收
内存分配策略
JMM(Java内存模型)中,将内存区域分为五个部分(图片中只有三个,并不太规范):
程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区
①程序计数器:线程私有,记录线程上下文(多线程是通过轮流切换和分配处理器时间片实现的),唯一没有OutOfMomery
②Java虚拟机栈(Stack):线程私有,与线程生命周期相同,线程创建的时候创建。主要存储方法的内容:局部变量、参数、返回值以及运算的中间结果等。
③本地方法栈:存储Native方法内容
④Java堆:线程共享,运行时内存区域,占用内存最大的地方,几乎所有的对象实例都存在这里。分为三个部分:年轻代(Eden、Survivor)、老年代
⑤方法区:线程共享,主要存储已经被java虚拟机加载的类的信息,包括运行时常量池、字段、方法信息、静态变量等。在编译时就已经分配好,存在与程序整个运行期间,不需要回收。
垃圾回收算法:
1.标记-清除算法 2.复制算法 3.标记-整理算法 4.分代收集算法
常见的内存问题
1.内存泄漏
2.内存抖动
3.图片Bitmap相关
4.代码质量&数量
5.日常不正确使用
1.内存泄漏
发生内存泄漏的本质原因:
本该回收的对象因为某些原因而不能被回收、从而继续停留在堆内存中
常见内存泄漏原因:
①集合类
②Static变量修饰的成员变量
③非静态内部类/匿名类
④资源对象使用后未关闭
具体了解
1.集合类导致内存泄漏:
集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄漏。
Android内存泄漏之集合类
// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null;
}
// 虽释放了集合元素引用的本身:o=null)
// 但集合List 仍然引用该对象,故垃圾回收器GC 依然不可回收该对象
解决方案:集合类添加集合元素对象后,在使用后必须从集合中删除(Android Studio检测内存泄漏是检测不到的!!!!!,所以我们必须手动释放)
TODO 验证!!
2.static关键字修饰的成员变量
这我想无需多言,被static关键字修饰的成员变量的生命周期 = 应用生命周期
常见的就是单例、静态context
3.非静态内部类的实例为静态的情况下
如果非静态内部类创建了静态的实例(其生命周期 = 应用生命周期),会因费静态内部类持有外部类的引用而导致外部类无法释放,最终造成内存泄漏。也就是
外部类中持有非静态内部类的静态对象
demo:
// 背景:
a. 在启动频繁的Activity中,为了避免重复创建相同的数据资源,会在Activity内部创建一个非静态内部类的单例
b. 每次启动Activity时都会使用该单例的数据
public class TestActivity extends AppCompatActivity {
// 非静态内部类的实例的引用
// 注:设置为静态
public static InnerClass innerClass = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 保证非静态内部类的实例只有1个
if (innerClass == null)
innerClass = new InnerClass();
}
// 非静态内部类的定义
private class InnerClass {
//...
}
}
// 造成内存泄露的原因:
// a. 当TestActivity销毁时,因非静态内部类单例的引用(innerClass)的生命周期 = 应用App的生命周期、持有外部类TestActivity的引用
// b. 故 TestActivity无法被GC回收,从而导致内存泄漏
解决方案:
将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
该内部类抽取出来封装成一个单例
尽量 避免 非静态内部类所创建的实例 = 静态
4.多线程导致的内存泄漏
多线程泄漏条件:当多线程使用为非静态内部类或者匿名类的时候可能导致内存泄漏。
这里的多线程包括:AsyncTask/实现Runnable接口/继承Thread类
泄漏原因:
当工作线程正在处理任务且外部类需销毁时,由于工作线程实例持有外部类引用,将使得外部类无法被垃圾回收器GC回收,从而造成内存泄漏。
demo:
/**
* 方式1:新建Thread子类(内部类)
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 通过创建的内部类 实现多线程
new MyThread().start();
}
// 自定义的Thread子类
private class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(5000);
Log.d(TAG, "执行了多线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 方式2:匿名Thread内部类
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 通过匿名内部类 实现多线程
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
Log.d(TAG, "执行了多线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
/**
* 分析:内存泄露原因
*/
// 工作线程Thread类属于非静态内部类 / 匿名内部类,运行时默认持有外部类的引用
// 当工作线程运行时,若外部类MainActivity需销毁
// 由于此时工作线程类实例持有外部类的引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露
从上面可以看出,造成内存泄漏的原因有2个关键条件:
1.存在 工作线程持有外部类的引用关系
2.工作线程实例的生命周期>外部类的生命周期,即工作线程还在运行,而外部类已经销毁了。
我们只需要破坏其中一条条件即可解决内存泄漏。
解决方案也有两种:
1.将内部线程类声明为静态内部类。
2.在onDestroy里调用Thread.stop()方法。
// 共有2个解决方案:静态内部类 & 当外部类结束生命周期时,强制结束线程
// 具体描述如下
/**
* 解决方式1:静态内部类
* 原理:静态内部类 不默认持有外部类的引用,从而使得 “工作线程实例 持有 外部类引用” 的引用关系 不复存在
* 具体实现:将Thread的子类设置成 静态内部类
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 通过创建的内部类 实现多线程
new MyThread().start();
}
// 分析1:自定义Thread子类
// 设置为:静态内部类
private static class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(5000);
Log.d(TAG, "执行了多线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 解决方案2:当外部类结束生命周期时,强制结束线程
* 原理:使得 工作线程实例的生命周期 与 外部类的生命周期 同步
* 具体实现:当 外部类(此处以Activity为例) 结束生命周期时(此时系统会调用onDestroy()),强制结束线程(调用stop())
*/
@Override
protected void onDestroy() {
super.onDestroy();
Thread.stop();
// 外部类Activity生命周期结束时,强制结束线程
}
Handler内存泄漏原因
Handler内存泄漏
Handler造成内存泄漏有两个关键条件:
1.存在"未被处理/正在处理的消息,持有非静态内部类Handler的实例(Handler又会持有外部类)"
2.Handler的生命周期 > 外部类的生命周期
即Handler消息队列中还有未处理的消息/正在处理消息 而 外部类又需要销毁。
解决思路:
只需要上面任一条件不成立即可。
①Handler声明为静态内部类,使用WeakReference持有外部类
②当外部类结束生命周期时,清空Handler内消息队列:
在onDestroy里清除Handler消息队列里的所有消息(调用removeCallbacksAndMessages(null))
5.资源对象使用后未关闭
泄漏原因:
对于资源的使用,如:
①广播BroadcastReceiver
②文件流File
③数据库游标Cursor
④图片资源Bitmap等
解决方案:
在Activity销毁时及时关闭/注销资源
①对于 广播BraodcastReceiver:注销注册
unregisterReceiver()
② 对于 文件流File:关闭流
InputStream / OutputStream.close()
③ 对于数据库游标cursor:使用后关闭游标
cursor.close()
④对于 图片资源Bitmap:Android分配给图片的内存只有8M,若1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null
Bitmap.recycle();
Bitmap = null;
⑤对于动画(属性动画)
将动画设置成无限循环播放repeatCount = “infinite”后
在Activity退出时记得停止动画
6.其他泄漏原因
总结
辅助分析内存泄漏的工具
1.MAT(Memory Analysis Tools)
2.Heap Viewer
3.Allocation Tracker
4.Android Studio 的Profiler
5.LeakCanary
其实后两者是最常用的,Android Studio自带的Profiler一直在迭代更新,4.1之后还会显示内存泄露的点,当然可能有些不全,但相信不久的将来,在Google工程师的努力下,会适配绝大多数场景。
LeakCanary 会将所有的内存泄露以通知栏的方式告知,很方便。
MAT的话,之前用过,还需要手动dump下来hprof文件,转成MAT需要的格式,操作起来比较麻烦。
HeapViewer 没用过,但是看使用文档,其具有的功能Android Profiler都有
Heap Viewer使用
Allocation Tracker:Profiler里检测内存的就叫Allocation Tracker...
Bitmap优化
在点三中有做过总结,概不赘述。
【其他小技巧】
1.获取当前可使用的内存大小
调用ActivityManager.getMemoryClass()方法可获取当前应用可用的内存大小(单位=兆)
2.【获取当前的内存使用情况】
在应用生命周期的任何阶段,调用onTrimMemory()获取应用程序当前内存使用情况,可根据该方法返回的内存紧张级别参数来释放内存。
onTrimMemory
① OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.
②哪些组件可以实现onTrimMemory回调:
Application.onTrimMemory()
Activity.onTrimMemory()
Fragment.OnTrimMemory()
Service.onTrimMemory()
ContentProvider.OnTrimMemory()
③onTrimMemory和onLowMemory的关系
onLowMemory是api <14的机器使用的
④onTirmMemory会传递不同的等级,等级值有很多,最常用的是:
TRIM_MEMORY_UI_HIDDEN
表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源onTrimMemory的TRIM_MEMORY_UI_HIDDEN 等级是在onStop方法之前调用的
我们项目中是在Application的onTirmMemory中释放掉Fresco的图片缓存。同时调用了System.gc(),手动GC一下。
onTrimMemory典型的使用场景
1.退出的时候,动态生成的View、图片缓存、Fragment可以及时释放
2.后台Service,用户退出UI界面后,可以及时释放部分UI和Cache
包体积优化
安装包组成结构:
①assets文件夹。存放一些配置文件、资源文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。
②res。res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。
③META-INF。保存应用的签名信息,签名信息可以验证 APK 文件的完整性。
AndroidManifest.xml。这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。
④classes.dex。Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。
⑤resources.arsc。记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。
①使用Android Lint删除冗余资源
Android Analyze
具体使用就是运行 Analyze -> Run Inspection by name
直接输入 unused ...就可以过滤各种没有使用的资源,比如:
unused resources
②使用TinyPNG压缩图片,一般体积至少能压百分之五十
原理:把相似像素的24bit位用8bit位来表示,并且移除了不必要的元数据,是有损压缩,不过肉眼很难看出来。
③不要引入过大的库
④代码混淆工具,如proGuard,含:压缩、优化、混淆等
其他
LeakCanary原理
监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收(在ReferenceQueue中说明可以被回收,不存在泄漏;否则,可能存在泄漏,LeakCanary是执行一遍GC,若还未在ReferenceQueue中,就会认定为泄漏)。
如果Activity被认定为泄露了,就抓取内存dump文件(Debug.dumpHprofData);之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析;接着通过HeapAnalyzer (checkForLeak—findLeakingReference---findLeakTrace)来进行内存泄漏分析。最后通过DisplayLeakService进行内存泄漏的展示。