性能优化属于一个老生常谈的话题,性能的优化直观角度上主要体现在UI的流畅程度、运行的稳定性、代码质量和安装包的体积大小。
布局绘制优化
GPU过度绘制开关
屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
在开发者模式中打开调试GPU过度绘制开关
,通过界面上的不同蒙层颜色查看界面不同区域的绘制情况
可以看到,一共分为四个等级,分别是蓝色,淡绿,淡红和深红。这几个重复绘制程度依次递增,简单来说就是减少红色区域,尽量向蓝色区域靠拢
可以看到,当开启过度绘制开关之后,就能看到app对应页面的过度绘制情况,而我们的布局优化就是从这里开始,尽量减少ui的嵌套层次,避免gpu过度绘制~
优化细节:
- 如果父控件有颜色,也是自己需要的颜色,那么就不必在子控件加背景颜色
- 如果每个自控件的颜色不太一样,而且可以完全覆盖父控件,那么就不需要在父控件上加背景颜色
- 尽量减少不必要的嵌套
- 使用include和merge增加复用,减少层级
- ViewStub按需加载,更加轻便
- 复杂界面可选择ConstraintLayout,可有效减少层级
GPU渲染模式分析
在开发者模式中打开 GPU渲染模式分析
的开关,查看界面的绘制情况
柱状图每一根代表一帧
随着界面的刷新,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。
中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。
-
柱状图注释:
在Android6.0之前,柱状图主要为黄色、红色、蓝色(Swap Buffers,Command Issue,Draw)三类。自从安卓6.0之后,增加至8条数据。新版本GPU呈现分析曲线新增加了(Sync&Upload,Measure&LayoutAnimation,Input Handling,Misc/Vsync Delay)五大步骤数据。
(1)Command Issue(红色):表示执行任务的时间,是Android进行2D渲染显示列表的时间,为了将内容绘制到屏幕上,Android需要使用Open GL ES的API接口来绘制显示列表,红色线条越高表示需要绘制的视图更多;比如我们在遇到多张图加载的时候,红色会突然跳的很高,此时滑动页面也就不流畅了,要等几秒图片才能加载出来,并不是卡住。
(2)Swap Buffers(黄色):表示处理任务的时间,即CPU等待GPU完成任务的时间,线条越高,表示GPU做的事情越多。若橙色部分过高,说明GPU目前过于忙碌。
(3)Draw(蓝色):表示测量和绘制视图列表所需要的时间,蓝色线条越高表示每一帧需要更新很多视图,或者View的onDraw方法中做了耗时操作。它越长说明当前视图比较复杂或者无效需要重绘,表现为卡顿。
理想的流畅状态是三色都低于绿线以下。
(4)Sync & Upload(浅蓝色):表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片的大小。
下面这几种统称为绿色,随着后面标注的数字颜色逐渐加深。
(5) Measure/Layout(绿色1)表示布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题;。
(6)Animation(绿色2):表示计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等。
(7)Input Handling(绿色3):表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。
(8) Misc Time/Vsync Delay(绿色4):表示在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况。
关于界面卡顿,其实主要原因就是渲染导致的,Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,但是渲染未必成功,如果成功了那么代表一切顺利,但是失败了可能就要延误时间,或者直接跳过去,给人视觉上的表现,就是要么卡了一会,要么跳帧。
View的绘制频率保证60fps是最佳的,这就要求每帧绘制时间不超过16ms(16ms = 1000/60),虽然程序很难保证16ms这个时间,但是尽量降低onDraw方法中的复杂度总是切实有效的。
在自定义view中:
onDraw方法中不要做耗时的任务,也不做过多的循环操作,特别是嵌套循环,虽然每次循环耗时很小,但是大量的循环势必霸占CPU的时间片,从而造成View的绘制过程不流畅。
onDraw()中不要创建新的局部对象,因为onDraw()方法一般都会频繁大量调用,就意味着会产生大量的零时对象,不进占用过的内存,而且会导致系统更加频繁的GC,大大降低程序的执行速度和效率。
内存优化
app的卡顿有相当一部分原因是因为频繁GC,导致了app产生卡顿,因为在GC线程开始执行的时候,所有线程都会停止工作,而越频繁,停止工作的时间就会越长,给用户最直观的感受就是卡顿。为了避免出现这一现象,那么就应该写出符合GC回收算法的代码,才能保证程序的稳定和流畅。
内存回收理论
内存回收其实是属于Java语言的内存自动管理机制,属于Java的一大特性。其中Java的垃圾回收算法目前大致有两种,一种是引用计数算法
:
- 首先给每一个对象都添加一个引用计数器
- 当程序的某一个地方引用了此对象,这个计数器的值就加1
- 当引用失效的时候(例如超过了作用域),这个计数器就减1
- 当某一个对象的计数器的值是0的时候,则判定这个对象不可能被使用
此算法有一个确定就是无法解决循环引用,当发生了循环引用,那么计数器始终大于等于1,那么这时候这块内存便不会得到回收,那么这时候就有了第二种垃圾回收算法,GC Root可达性分析算法
:
- 以称作“GC Root”的对象作为起点向下搜索
- 每走过一个对象,就生成一条引用链
- 从根开始到搜索完,生成了一棵引用树,那些到GC Root不可达的对象就是可以回收的
而程序中可作为GC ROOT对象的分为: - 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
以上两种算法判定了一个对象是否有必要进行回收,而实际上开始执行垃圾回收的算法则是:
- 标记清除算法(Mark-Sweep)
- 复制算法(Copying)
- 标记整理算法(Mark-Compact)
- 分代回收算法
标记清除算法(Mark-Sweep)
对将要回收的对象进行标记,当遍历完所有对象后,对标记的对象进行清理。
这当中会有一个缺陷,在清理完之后,不会对剩余内存做整理,这样就会出现参差不齐内存空间的内存碎片,久而久之,GC的触发就会变频繁,导致程序卡顿
复制算法(Copying)
复制算法可以解决上述产生大量内存碎片的问题,申请两倍的内存存储空间,一半用于存储对象,一半保留,当开始清理的时候,将未被标记的对象复制到另外一半内存中,然后对剩下的空间直接进行清理
虽然解决了内存碎片的问题,但是还会带来一个新的问题,就是浪费了内存空间,好好的内存实际使用只有一半
标记整理算法(Mark-Compact)
标记整理则是对标记清除的后半段做了一个优化,即优化了内存碎片问题
将没有被标记的对象放到内存的另一端,然后清理剩下的内存,相较于之前的算法,这个算法多出了一步空间整理,效率上有所欠缺
分代回收算法
首先第一种产生内存碎片的算法相较于后两种算法毫无优势可言,直接排除,而针对复制算法和标记整理算法的特点,针对不同的应用环境来搭配使用这两种算法,则可以对回收调优。当存活对象少,清理对象多的时候,仅需要直接复制数量少的存活对象到另外内存中,然后直接清理剩余空间,效率较高,而反之,使用标记整理效率较高,两种算法搭配使用可以使得回收效率提高。
- 对于新生代区域采用复制算法,因为新生代中每次垃圾回收都要回收大部分对象,那么也就意味着复制次数比较少,因此采用复制算法更高效
- 而老年代区域的特点是每次回收都只能回收很少的对象,一般使用的是标记整理或者标记清除算法
释:
Young(年轻代):
Eden(伊利园):存放新生对象。
Survivor(幸存者):存放经过垃圾回收没有被清除的对象。
Tenured(老年代):对象多次回收没有被清除,则移到该区块。
Perm:存放加载的类 还有方法对象。
触发GC的类型
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。
内存回收实例
内存的优化要解决的就是内存泄露的问题,在Android中常常碰到的内存泄露大致为: 1、集合类泄漏 2、单例/静态变量造成的内存泄漏 3、匿名内部类/非静态内部类 4、资源未关闭造成的内存泄漏
在检测Android中的内存泄露时,可以使用leakCanary工具类查看应用的内存泄露情况
集合类泄露
集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露。
static List<Object> mList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Object obj = new Object();
mList.add(obj);
obj = null;
}
这里的list就会持续持有着大量的对象,当gc在回收添加到list中的对象的时候,就会从root中找到list的引用,从而不会回收object,导致了内存泄露。
单例/静态变量造成的内存泄漏
这里的内存泄露其实归纳来说就是生命周期长的对象持有了生命周期端的对象
,当gc回收生命周期短的对象的时候,就会发现有生命周期长的对象还在引用着这个生命周期短的对象,那么就不会回收生命周期短的对象
public class SingleInstance {
private static SingleInstance mInstance;
private Context mContext;
private SingleInstance(Context context){
this.mContext = context;
}
public static SingleInstance newInstance(Context context){
if(mInstance == null){
mInstance = new SingleInstance(context);
}
return sInstance;
}
}
针对于单例,其实单例的生命周期相当于整个app的生命周期,所以这里只需要将传递的context对象置为app的上下文即可。
public static SingleInstance newInstance(Context context){
if(mInstance == null){
mInstance = new SingleInstance(context.getApplicationContext());
}
return sInstance;
}
匿名内部类/非静态内部类
非静态内部类他会持有他外部类的引用,非静态内部类的生命周期可能比外部类更长,如果非静态内部类的周明周期长于外部类,在加上自动持有外部类的强引用那么自然发生泄漏。
我们常说到的handler泄漏就属于匿名内部类泄漏,如下:
public class TestActivity extends Activity {
private TextView mText;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
mText.setText(" do someThing");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mText = findVIewById(R.id.mText);
mHandler. sendEmptyMessageDelayed(0, 100000);
}
在100s之前销毁该activity,那么就会发现TestActivity是无法被回收的,因为handler的回调接口中持有的activity中的tv的引用,那么这时候activity就无法被回收,发生了泄漏,而这里要解决这样的泄漏,第一步,因为handler的生命周期可能比activity要长,所以先将内部类作为静态类。第二步,因为在回调中将持有的activity对象传递到handler中弱持有。
public class TestActivity extends Activity {
private TextView mText;
private MyHandler myHandler = new MyHandler(TestActivity.this);
private MyThread myThread = new MyThread();
private static class MyHandler extends Handler {
WeakReference<TestActivity> weakReference;
MyHandler(TestActivity testActivity) {
this.weakReference = new WeakReference<TestActivity>(testActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
weakReference.get().mText.setText("do someThing");
}
}
private static class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mText = findViewById(R.id.mText);
myHandler.sendEmptyMessageDelayed(0, 100000);
myThread.start();
}
//最后清空这些回调
@Override
protected void onDestroy() {
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
资源未关闭造成的内存泄漏
- 网络、文件等流忘记关闭
- 手动注册广播时,退出时忘记 unregisterReceiver()
- Service 执行完后忘记 stopSelf()
- EventBus 等观察者模式的框架忘记手动解除注册
qa:
1、除了布局优化和内存优化,在Android开发中还能从什么角度进行优化?
2、Android中可以通过什么工具精准定位内存泄露的发生?
启动优化
启动状态
热启动
热启动是三种启动状态中是最快的一种,因为热启动是从后台切到了前台,不需要再创建 Applicaiton,也不需要再进行渲染布局等操作。
暖启动
暖启动的启动速度介于冷启动和热启动之间,暖启动只会重走 Activity 的生命周期,不会重走进程创建和 Application 的创建和生命周期等。
冷启动
从头创建app过程:
创建进程:
- 启动app
- 加载空白window
- 创建进程
启动应用
- 创建appliction
- 启动主线程
- 创建MainActivity
绘制界面
- 加载布局
- 布置屏幕
- 首帧绘制
测量启动
命令测量:
adb shell am start -W packageName/首屏Activity
adb shell am start -W com.done/com.done.MainActivity
ThisTime 代表最后一个 Activity 启动所需要的时间,也就是最后一个 Activity 的启动耗时。
TotalTime 代表所有 Activity 启动耗时。
WaitTime 是 AMS 启动 Activity 的总耗时。
ThisTime <= TotalToime < WaitTime
代码测量:
埋点进行记录测量,通过在applicaiton的onAttachContext时机开始计时,在Launcher中的View调用preDraw的时候停止计时
public class Test {
private static final String TAG = "Test";
private static long time;
public static void startRecordTime() {
time = SystemClock.currentThreadTimeMillis();
}
public static void stopRecordTime() {
time = SystemClock.currentThreadTimeMillis() - time;
Log.d(TAG, "stop Record Time: " + time);
}
}
分析工具
Traceview
使用方式
通过 Debug.startMethodTracing("输出文件") 就可以开始跟踪方法,记录一段时间内的 CPU 使用情况,调用 Debug.stopMethodTracing() 停止跟踪方法后,系统就会为我们生成一个文件,通过 Traceview 查看这个文件记录的内容
@Override
public void onCreate() {
super.onCreate();
mAppContext = this;
mHandler = new Handler(Looper.getMainLooper());
Debug.startMethodTracing(getExternalCacheDir() + "/myTrace.trace");
initThirdSdk();
Debug.stopMethodTracing();
}
/**
* 初始化三方SDK
*/
private void initThirdSdk() {
initUtils();
initRoute();
initNetwork();
initBugly();
initLeakCanary();
}
结果分析
把保存的文档导出来,直接拖到Android Studio中,profile会直接解析出来~
Systrace
Systrace 结合了 Android 内核数据,分析了线程活动后会给我们生成一个非常精确 HTML 格式的报告。
同理Systrace在想要检测的环节发起调用
@Override
public void onCreate() {
super.onCreate();
mAppContext = this;
mHandler = new Handler(Looper.getMainLooper());
RecordTimeUtils.startRecord();
TraceCompat.beginSection("Done_Done_Done_onAppCreate");
initThirdSdk();
TraceCompat.endSection();
}
终端进入到sdk的目录下,然后运行Systrace的命令
python systrace.py -t 10 -o trace.html
- -t: 标识跟踪时间
- -o: 把文件输出到指定目录,这里直接输出到当前目录下
-
-a: 标识要启动的应用包名
在浏览器中打开
优化方法
闪屏页
闪屏页的优化其实只是给用户一个视觉上的加快速度效果,点开就能看到app的启动页面,而闪屏就是利用创建空白window的时机创建一个占位图
。让用户有一个点开即开的赶脚。闪屏配置主要是四个步骤:
- 定义闪屏图
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/cardview_light_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/house_detail_loading_bg_little" />
</item>
</layer-list>
- 定义闪屏主题
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="Splash" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">
@drawable/splash_bg
</item>
<item name="android:windowFullscreen">true</item>
<item name="windowNoTitle">true</item>
</style>
</resources>
- Launcher Activity配置theme
<activity
android:name="com.done.worldbuddy.MainActivity"
android:theme="@style/Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- 换回主题
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
}
异步初始化
可以利用子线程方式进行三方的初始化,可以是线程池
,可以使IntentService,可以是懒加载。主要还是结合项目进行异步初始化的操作
- 三方的初始化可以是懒加载的方式,如地图sdk
- 对不需要及时使用且不需要在MainTask中初始化的任务放在线程中进行执行或者延迟放到欢迎页面进行初始化操作
apk瘦身
图片格式优化
WebP 是一种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式 VP8。WebP 最初在2010年发布,目标是减少文件大小,但达到 和 JEPG 格式相同的图片质量,希望能够减少图片档在网络上的发送时间。2011年11月8日,Google 开始让 WebP 支持无损压缩和透明色的功能。
根据 Google 较早的测试,WebP 的无损压缩比网络上找到的 PNG 档少了 45% 的文件大小,即使这些 PNG 档在使用 PNGCRUSH 和 PNGOUT 处理过,WebP 还是可以减少 28% 的文件大小。就目前而言,Webp 可以让图片大小平均减少 70% 。WebP 是未来图片格式的发展趋势
多语言
android{
...
defaultConfig{
...
//只保留英语
resConfigs "en"
}
}
去除冗余的资源文件
as利用Analyze > Run Inspection By Name > unused resources
注意:如果是动态获取资源文件也会被断定为冗余资源
//动态获取资源 id , 未直接使用 R.xx.xx ,则这个 id 代表的资源会被认为没有使用过(类似不能混淆反射类)
int indetifier =getResources().getIdentifier("img_bubble_receive", "drawable", getPackageName()); getResources().getDrawable(indetifier);
所以删除之前,可以全局搜索一下这个文件名字有没有出现在java代码中
打包移除无用资源
buildTypes {
release {
minifyEnabled true
shrinkResources = true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
shrinkResources = true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
去除多cpu架构so库
代码混淆
release{
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug{
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
AndResGuard 微信资源压缩方案
AndResGuard是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。
AndResGuard不涉及编译过程,只需输入一个apk(无论签名与否,debug版,release版均可,在处理过程中会直接将原签名删除),可得到一个实现资源混淆后的apk(若在配置文件中输入签名信息,可自动重签名并对齐,得到可直接发布的apk)以及对应资源ID的mapping文件
使用wiki
apply plugin: 'AndResGuard'
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'
}
}
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
// for your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for google-services
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.17'
//path = "/usr/local/bin/7za"
}
/**
* 可选: 如果不设置则会默认覆盖assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可选: 指定v1签名时生成jar文件的摘要算法
* 默认值为“SHA-1”
**/
// digestalg = "SHA-256"
}
白名单
所有使用getIdentifier访问的资源都需要加入白名单。
question:
- 有哪些启动状态,分别是什么含义,对异步执行同步返回的思考
- apk瘦身的方式
电量优化
Battery-Historian
源码依赖方式,所需环境:
- GO环境
- Python环境
- Git环境
-
Java环境