一、渲染问题
Google官方说过,大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性
Android系统每隔16ms发出场同步信号,触发对UI进行渲染,在Android系统中是以每秒60帧为满帧的,那么1秒÷60帧,就能得出每帧为16ms时为满帧的界限,每帧快于16ms即为流畅,如果你的某个操作花费时间是大于16ms,系统在得到场同步信号的时候就无法进行正常渲染,这样就发生了丢帧现象
先来看看造成应用UI卡顿的常见原因都有哪些?
1、布局Layout过于复杂,无法在16ms内完成渲染
2、同一时间动画执行的次数过多,导致CPU或GPU负载过重
3、View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重
4、View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染
5、内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作
6、ANR
(1)过度绘制
过度绘制指屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次,这就浪费了CPU以及GPU资源,导致界面滑动不流畅、界面启动速度慢等问题,针对这个问题,我们可以在手机开发者选项—调试GPU过度绘制,查看过度绘制,比如:
各种颜色代表的意思:
为了避免过度绘制,我们可以从以下两个方面进行优化:
1.移除或修改Window默认背景,移除XML布局文件中非必需的背景
<!-- 可以添加通用主题背景 -->
<item name="android:windowBackground">@drawable/common_bg</item>
<!-- 去除背景 -->
<item name="android:windowBackground">null</item>
注意:弹窗的绘制是属于剪切式绘制不是覆盖绘制,蒙层是透明度亮度的调节不是绘制一层灰色。如果我们不想用系统dialog而是自定义一个弹窗view,就需要考虑过度绘制问题
2.自定义View优化,使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制
canvas.clipRect(0, 0, width*2, height*2);
(2)布局优化
布局优化就是减少布局文件层级,层级减少了,那么程序绘制时就快了许多,所以可以提高性能
1.如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多的CPU时间,但是如果布局需要通过嵌套的方式来完成。这种情况下还是建议采用RelativeLayout,因为ViewGroup的嵌套就相当于增加了布局的层级,同样会降低程序的性能
2.使用<include>和<merge>标签或者ViewStub,提取布局中公共部分的布局,可提高布局初始化效率,例如:
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/mainlayout_button_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="主布局第一个Button"/>
<include layout="@layout/test_layout"/>
</LinearLayout>
test_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/testlayout_button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试布局中Button1"/>
<Button
android:id="@+id/testlayout_button_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试布局第二个Button2"/>
</LinearLayout>
接下来把test_layout.xml布局节点改成merge,activity_main.xml布局不变
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/testlayout_button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试布局中Button1"/>
<Button
android:id="@+id/testlayout_button_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试布局第二个Button2"/>
</merge>
由此可以看出,三个Button处在一个线性布局下面,而没有使用merge节点的则是Button2和Button3处在另外一层LinearLayout,因此可以很清楚的看出merge最大的好处就是减少布局嵌套,但是merge里面的控件的布局方式(垂直或者是水平)并不能自己控制,它的布局方式受制于容纳include的布局
ViewStub的使用
步骤一:定义需要懒加载的布局 test.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#36e7ea"
android:gravity="center"
android:textSize="18sp" />
</LinearLayout>
步骤二:使用ViewStub引入要懒加载的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="加载ViewStub" />
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/test" />
</LinearLayout>
步骤三:使用代码按需加载上面的ViewStub布局
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
val view: View = (findViewById<View>(R.id.view_stub) as ViewStub).inflate()
//加载出来用view接收
val tv: TextView = view.findViewById(R.id.text_view) as TextView //然后可以用view访问它的子控件
tv.setText("ViewStub")
}
})
不过使用ViewStub时要注意,ViewStub对象只可以inflate一次,之后ViewStub对象会被置为空,所以当需要在运行时不止一次的显示和 隐藏某个布局,那么ViewStub是做不到的。ViewStub不支持merge标签,意味着你不能引入包含merge标签的布局到ViewStub中
看到这,有人可能要问了,这和visibility="gone"有什么区别呢?
其实是有的,设置某个布局模块为gone,系统在渲染该布局时还是会去计算这个布局模块的宽高等属性,还是会把它添加到布局树上。因此这个布局模块还是会占有渲染布局的部分时间,而把该布局模块放在ViewStub,系统在渲染该布局时并不会去理ViewStub节点,因此可以节省渲染布局模块的时间。只有当需要展示时,才会去渲染
(3)查看渲染性能的工具
开发者选项 — GPU呈现模式分析 — 选择“在屏幕上显示为条形图”
绿线所标示的高度即为16ms线,低于绿线即为流畅。红色代表了“执行时间”,它指的是Android渲染引擎执行盒子中这些绘制命令的时间, 黄色通常较短,它代表着CPU通知GPU“你已经完成视图渲染了”,蓝色代表了视图绘制所花费的时间,当它越短体验上更接近“丝滑”,用来判断流畅度的参考意义最大
二、绘制优化
绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面:
(1)onDraw中不要创建新的局部对象,因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率
(2)onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅
三、内存优化
三者的区别:
OOM是当前占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存限制
内存抖动是短时间内大量的对象被创建,然后又被马上释放
内存泄漏指无用对象持续占有内存或得不到及时释放,从而造成的内存空间的浪费,过多的内存泄漏会造成OOM
1. 内存泄漏
内存泄漏是指一些不用的对象被长期持有,导致内存无法被释放
由于 存在垃圾回收机制(GC),理应不存在内存泄露;出现内存泄露的原因为人为原因,无意识地持有对象引用,使得持有引用者的生命周期 > 被引用者的生命周期
常见的内存泄露原因和解决方案
(1)集合类
集合类 添加元素后,仍引用着 集合元素对象,导致该集合元素对象不可被回收,从而导致内存泄漏
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null;
}
// 虽释放了集合元素引用的本身:o=null
// 但集合List 仍然引用该对象,故垃圾回收器依然不可回收该对象
解决方案
// 释放objectList
objectList.clear();
objectList = null;
(2)Static关键字修饰的成员变量
static 修饰的成员变量的生命周期 = 应用程序的生命周期,若使被 static 修饰的成员变量 引用耗费资源过多的实例(如Context),当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露,此种情况有个非常典型的例子,那就是单例模式
// 若传入的是Activity的Context,此时单例则持有该Activity的引用
// 由于单例一直持有该Activity的引用,直到整个应用生命周期结束,即使该Activity退出,该Activity的内存也不会被回收
// 特别是一些庞大的Activity,这样容易导致OOM
public class SingleInstanceClass {
private static SingleInstanceClass instance;
private Context mContext;
private SingleInstanceClass(Context context) {
this.mContext = context; // 传递的是Activity的context
}
public SingleInstanceClass getInstance(Context context) {
if (instance == null) {
instance = new SingleInstanceClass(context);
}
return instance;
}
}
解决方案
// 传递Application的context,因为Application的生命周期等于整个应用的生命周期
public class SingleInstanceClass {
private static SingleInstanceClass instance;
private Context mContext;
private SingleInstanceClass(Context context) {
this.mContext = context.getApplicationContext();
}
public SingleInstanceClass getInstance(Context context) {
if (instance == null) {
instance = new SingleInstanceClass(context);
}
return instance;
}
}
(3)非静态内部类,匿名类
非静态内部类,匿名类默认持有外部类的引用;而静态内部类则不会
当非静态内部类遇上多线程
/**
* 方式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();
}
}
}
}
//分析:内存泄露原因
// 工作线程Thread类属于非静态内部类 / 匿名内部类,运行时默认持有外部类的引用
// 当工作线程运行时,若外部类MainActivity需销毁
// 由于此时工作线程类实例持有外部类的引用,将使得外部类无法被GC回收,从而造成内存泄露
Handler造成的内存泄漏
可移步我的另一篇博文https://blog.csdn.net/qq_45485851/article/details/105469389
(4)资源对象使用后未关闭
对于资源的使用(如文件流File、数据库游标Cursor、图片资源Bitmap等),若在Activity销毁时无及时关闭或者注销这些资源,则这些资源将不会被回收,从而造成内存泄漏
// 对于 广播BraodcastReceiver:注销注册
unregisterReceiver()
// 对于 文件流File:关闭流
InputStream / OutputStream.close()
// 对于数据库游标cursor:使用后关闭游标
cursor.close()
// 对于 图片资源Bitmap:Android分配给图片的内存只有8M,若1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null
Bitmap.recycle();
Bitmap = null;
(5)OOM
OOM常见的原因有两个:加载大图,内存泄漏,内存泄漏已经讲了,现在主要讲讲加载大图吧
图片的压缩
对于分辨率比我们手机屏幕的分辨率高得多的图片,我们应该加载一个缩小版本的图片,从而避免超出程序的内存限制,BitmapFactory这个类提供了多个解析方法用于创建Bitmap对象,比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id., options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;//设置BitmapFactory.Options中inSampleSize的值来压缩图片
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;//注意: inSampleSize的取值应该是2的指数
}
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
最后把压缩的图片显示出来即可
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
图片缓存(LruCache)
主要算法原理是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除
构造一个工具类,用来存储图片到缓存和从缓存中读取图片
public class CustomLruCache {
private LruCache<String, Bitmap> stringBitmapLruCache;
int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取最大内存
int cacheSize = maxMemory / 16;//大小为最大内存的1/16
private static CustomLruCache customLruCache;
/**
* 私有化构造方法
*/
private CustomLruCache() {
stringBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
/**
* 单例模式获取实例,保证只有一个CustomLruCache对象,同时保证只有一个CustomLruCache.stringBitmapLruCache
*
* @return
*/
public static CustomLruCache getInstance() {
if (customLruCache == null) {
customLruCache = new CustomLruCache();
}
return customLruCache;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) != bitmap)//如果缓存中不存在bitmap,就存入缓存
stringBitmapLruCache.put(key, bitmap);
}
public Bitmap getBitmapFromMemoryCache(String key) {
return stringBitmapLruCache.get(key);
}
}
public class CustomLruCache {
private LruCache<String, Bitmap> stringBitmapLruCache;
int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取最大内存
int cacheSize = maxMemory / 16;//大小为最大内存的1/16
private static CustomLruCache customLruCache;
/**
* 私有化构造方法
*/
private CustomLruCache() {
stringBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
/**
* 单例模式获取实例,保证只有一个CustomLruCache对象,同时保证只有一个CustomLruCache.stringBitmapLruCache
*
* @return
*/
public static CustomLruCache getInstance() {
if (customLruCache == null) {
customLruCache = new CustomLruCache();
}
return customLruCache;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) != bitmap)//如果缓存中不存在bitmap,就存入缓存
stringBitmapLruCache.put(key, bitmap);
}
public Bitmap getBitmapFromMemoryCache(String key) {
return stringBitmapLruCache.get(key);
}
}
AsyncTask<String ,Void,Bitmap> bitmapAsyncTask = new AsyncTask<String, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(String... params) {
Bitmap bitmap = null;
try {
CustomLruCache customLruCache = CustomLruCache.getInstance();
bitmap = customLruCache.getBitmapFromMemoryCache(params[0]);
//先从缓存中读取图片,如果缓存中不存在,再请求网络,从网络读取图片添加至LruCache中
//启动app后第一次bitmap为null,会先从网络中读取添加至LruCache,如果app没销毁,再执行读取图片操作时
//就会优先从缓存中读取
if (bitmap == null) {
//从网络中读取图片数据
URL url = new URL(params[0]);
bitmap = BitmapFactory.decodeStream(url.openStream());
//添加图片数据至LruCache
customLruCache.addBitmapToMemoryCache(params[0], bitmap);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
imageView.setImageBitmap(bitmap);
}
};
//加载图片
bitmapAsyncTask.execute(imageURL);
(6)内存抖动
(7)内存优化工具
AndroidStudio自带工具:Profiler
第三方工具:LeakCanary
https://square.github.io/leakcanary
原理概述:通过监听Activity的onDestory,手动调用GC,然后通过ReferenceQueue+WeakReference,来判断对象是否被回收WeakReference创建时,可以传入一个ReferenceQueue对象,假如WeakReference中引用对象被回收,那么就会把WeakReference对象添加到ReferenceQueue中,可以通过ReferenceQueue中是否为空来判断,被引用对象是否被回收,然后其分析泄露的位置
AndroidStudio Lint
静态代码分析工具
四、响应速度优化(ANR)
响应速度优化的核心思想就是避免在主线程中做耗时操作。如果有耗时操作,可以开启子线程执行,即采用异步的方式来执行耗时操作。Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver如果10秒钟之内还未执行完操作也会出现ANR。为了避免ANR,可以开启子线程执行耗时操作,但是子线程不能更新UI,需要子线程与主线程进行通信来解决。这涉及到Handler消息机制,AsyncTask,IntentService。当一个进程发生了ANR之后,系统会在/data/anr目录下创建一个文件traces.txt,通过分析这个文件就能定位出ANR的原因
五、启动优化
在冷启动开始时,系统有三个任务。这些任务是:
(1)加载并启动应用程序
(2)启动后立即显示应用程序空白的启动窗口
(3)创建应用程序进程
系统默认会在启动应用程序的时候启动空白窗口,直到 App 应用程序的入口 Activity 创建成功,视图绘制完毕,因为App应用进程的创建过程是由手机的软硬件决定的,所以我们只能在这个创建过程中视觉优化
1.设置闪屏图片主题
为了更顺滑无缝衔接我们的闪屏页,可以在启动 Activity 的 Theme中设置闪屏页图片,这样启动窗口的图片就会是闪屏页图片,而不是白屏,但是这种方式并没有真正的加速应用进程的启动速度,而只是通过用户视觉效果带来的优化体验
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/lunch</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item><!--显示虚拟按键,并腾出空间-->
</style>
2.Application 优化
Application是程序的主入口,很多第三方SDK示例程序中都要求自己在Application OnCreate时做初始化操作,尽量避免在Application onCreate时做初始化操作。可行的解决方案就是对第三方SDK实行懒加载,在真正用到的时候再去加载,某些组件的初始化也可以放在子线程中,例如友盟。同时避免在主线程做耗时操作,例如IO相关的逻辑,这样都会影响到应用启动速度
六、APK瘦身
1.清理无用资源
(1)使用Lint工具
要注意的点:检测没有用的布局并且删除,把未使用到的资源删除,String.xml有一些没有用到的字符也删除掉
(2)资源的动态加载,可以考虑在线加载模式,或者,如果模块过大,apk体积过大,可以考虑插件化来减小体积
(3)代码混淆,因为它可以删除注释和不用的代码;将java文件名改成短名;将方法名改成短名
android {
buildTypes {
release {
minifyEnabled true
}
}
}
(4)使用微信AndResGuard,github源地址是https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md
七、线程优化
线程优化的思想是采用线程池,线程池可以重用内部的线程,从而避免了线程的创建和销毁所带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而导致阻塞现象的发生。因此在实际开发中,我们要尽量采用线程池,而不是每次都要创建一个Thread对象
八、电量优化
首先我们先来搞清楚什么时候最耗电
(1)唤醒屏幕
当用户点亮屏幕的时候,意味着系统的各组件要开始进行工作,界面也需要开始执行渲染,此时会出现一条电量使用高峰线
(2)CPU唤醒使用
CPU唤醒时会出现高峰线,当工作完成后,设备又会主动进行休眠
(3)蜂窝式无线
当设备通过无线网发送数据的时候,为了使用硬件,这里会出现一个唤醒耗电高峰。接下来还有一个峰值,这是发送数据包消耗的电量,然后接受数据包也会消耗大量电量,也会有一个峰值
Google电量分析工具 Battery-Historian
我们都知道屏幕的渲染及 CPU的运行是耗电的主要因素之一。所以当我们在做内存优化、渲染优化、计算优化的时候,就已然在做电量优化,所以在平时的开发中,我们要注意点滴性能的优化积累