Android 性能优化之旅1--基本概念
前言
性能优化主要是经验之谈,以及结合工具给出的线索来分析,最后优化我们的代码,主要有两个方面:
- 如何去优化现有的项目,运行更加流畅。
- 以后开发项目的时候要从一开始就应该注意好性能方面的问题。
在Android中,每一个APP就是一个进程,每个进程都会有一个虚拟机实例,根据不同的手机而言,其分配的内存可能有8M,16M,32M,64M。其中比较常见的就是8M。性能优化里面比较重要的方面就是内存方面的优化问题,虽然一部手机的内存可能会比较大,但是由于每一个APP进程的内存分配的空间都是有限的,因此性能问题尤其需要注意。
在C/C++中,程序员需要自己去分配内存和释放内存--手动管理,例如malloc free。但是在Java中,内存的管理是交给虚拟机去做的,通过GC来管理内存会有一些不确定性的因素,很多时候是我们主观意愿觉得一个对象应该被回收,但是实际情况不然,这就很可能发生了内存泄漏,关于内存泄漏的知识会在下面讲解。
内存与内存泄露相关知识
java的GC内存回收机制:
某对象不再有任何的引用的时候才会进行回收。
什么是内存泄露:
内存不在GC掌控之内了。当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。
内存分配的几种策略:
1.静态的(生命周期比较长)
静态的存储区:内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在。
它主要存放静态数据、全局的static数据和一些常量。
2.栈式的
在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉。
栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。
3.堆式的
也叫做动态内存分配。有时候可以用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放(java里面直接依赖GC机制)。
在C/C++这里是可以自己掌控内存的,需要有很高的素养来解决内存的问题。java在这一块貌似程序员没有很好的方法自己去解决垃圾内存,需要的是编程的时候就要注意自己良好的编程习惯。
推荐阅读:深入理解JVM虚拟机
栈与堆的区别:
堆是不连续的内存区域,堆空间比较灵活也特别大。
栈式一块连续的内存区域,大小是有操作系统觉决定的。
其中,堆的管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。
对于栈的话,他是一种先进后出的数据结构,进出完全不会产生碎片,运行效率高且稳定。
主要是注意堆的使用,因为栈是JVM管理的
项目应该在一开始的时候就应该养成良好的习惯。
优化:当APP比较卡的时候,如果你不知道在哪里会有内存泄漏的时候,需要使用工具来帮助我们分析代码。常见的有AS自带的工具、MAT
栗子:
public class Main{
int a = 1;//成员变量的基本数据类型都在堆中
Student s = new Student();//成员变量引用以及引用的实体都在堆中
public void XXX(){
int b = 1;//方法里面的局部变量栈里面
Student s2 = new Student();//方法里面的局部变量的引用在栈里面,实体在堆里面
}
}
- 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)---因为他们属于类,类对象最终还是要被new出来的。
- 局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。-----因为他们属于方法当中的变量,生命周期会随着方法一起结束。
我们所讨论内存泄露,主要讨论堆内存,他存放的就是引用指向的对象实体。
有时候确实会有一种情况:当需要的时候可以访问,当不需要的时候可以被回收也可以被暂时保存以备重复使用(缓存起来)。
比如:ListView或者GridView、REcyclerView加载大量数据或者图片的时候,图片非常占用内存,一定要管理好内存,不然很容易内存溢出。滑出去的图片就回收,节省内存。看ListView的源码----回收对象,还会重用ConvertView。如果用户反复滑动或者下面还有同样的图片,就会造成多次重复IO(很耗时),那么需要缓存---平衡好内存大小和IO,算法和一些特殊的java类。
常用的算法:lrucache(最近最少使用先回收)
特殊的java类(引用):使用不同的引用类型有利于回收(不回收)
- StrongReference强引用:
回收时机:从不回收 使用:对象的一般保存 生命周期:JVM停止的时候才会终止 - SoftReference,软引用
回收时机:当内存不足的时候;使用:SoftReference<String>结合ReferenceQueue构造有效期短;生命周期:内存不足时终止 - WeakReference,弱引用
回收时机:在垃圾回收的时候;使用:同软引用; 生命周期:GC后终止 - PhatomReference 虚引用
回收时机:在垃圾回收的时候;使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动; 生命周期:GC后终止
其中,软引用、弱引用用得比较多,虚引用随时都会被回收,一个比一个弱(回收级别)
开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用和弱引用。
但是在Android2.3以后,软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。因此推荐使用LRU算法,例如LruCache。
使用场景:大量的图片显示的时候,
ListView或者GridView、REcyclerView要使用内存缓存+外部缓存(SD卡)
内存泄漏案例
1、静态变量,类加载的时候初始化,类卸载(进程被杀死的时候)释放
public class CommonUtils {
private static CommonUtils sInstance;
private Context mContext;
private CommonUtils(Context context) {
mContext = context;
}
public static CommonUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new CommonUtils(context);
}
return sInstance;
}
}
2、非静态内部类,改为静态内部类,因为不会持有非静态的引用 Thread 或者Timer
方法加上static 或者 自定义静态类继承Thread
延时的,线程的,比Activity的生命周期更加长(归根结底是因为生命周期不一致导致的)
//也是匿名内部类,也会引用外部
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
break;
}
}
};
//非静态(匿名)内部类不正确使用会导致内存泄漏
//已经隐式持有了Activity
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
// LeakActivity.this.a = 10;
a = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
又如:
//可以在Activity 销毁的时候cancel,但是有时候Activity会突然挂掉,不走生命周期,因此还是保持良好的编码习惯
new Timer().schedule(new TimerTask() {
@Override
public void run() {
}
}, 50000);
3、资源没有正确关闭。例如:传感器SensorManager 蓝牙等、广播、自定义控件的时候Attribute资源获取之后没有recycle。
SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);//使用完毕以后应该反注册
Sensor s = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(null, 0);
4、不需要用的监听没有移除,尤其是观察者模式的时候,addXXXListener的时候(因为是添加到集合里面的)
/**
* 这个类需要防止内存泄漏,在ActivityonStop的时候清理所有观察者
*/
public class ListenerController {
//此种Map的特点是:当除了自身有对KEY的引用之外,此Key没有其他引用那么这个Map会丢弃这个值
private static WeakHashMap<View, View.OnClickListener> mListener = new WeakHashMap<>();
public static void setListener(View view, View.OnClickListener listener) {
mListener.put(view, listener);
}
public static void clearListener() {
mListener.clear();
}
}
又如:
final MyView view = new MyView(this);
//监听View的加载,计算宽高,计算完一定要移除。实际上是放到集合里面,add 要注意 set的就算了
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
}
});
5、无限循环动画,在Activity不可见的时候(onStop)应该停止,在onRestart的时候重新开启。
扩展:论Handler的正确使用姿势
典型错误的使用示例:
public class LeakActivity extends AppCompatActivity {
private int a = 10;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
mHandler.sendEmptyMessageDelayed(0, 5000);
}
//也是匿名内部类,也会引用外部
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
a = 20;
break;
}
}
};
}
分析:这是我们用得最多的用法,Handler隐式地引用了Activity(通过变量a)。Handler的生命周期有可能与Activity的生命周期不一致,比如栗子中的sendEmptyMessageDelayed,在5000毫秒之后才发送消息,但是很有可能这时候Activity被返回了,这样会造成Handler比Activity还要长寿,这样会导致Activity发生暂时性的内存泄漏。
姿势一:
为了解决这个问题,我们可以把Handler改为static的,但是这样会造成Handler无法访问Activity的非静态变量a,但是实际开发中我们的确是需要访问a,那怎么办呢?继续看下去吧!
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
//a = 20; //不能访问得到
break;
}
}
};
###前言
性能优化主要是经验之谈,以及结合工具给出的线索来分析,最后优化我们的代码,主要有两个方面:
- 如何去优化现有的项目,运行更加流畅。
- 以后开发项目的时候要从一开始就应该注意好性能方面的问题。
在Android中,每一个APP就是一个进程,每个进程都会有一个虚拟机实例,根据不同的手机而言,其分配的内存可能有8M,16M,32M,64M。其中比较常见的就是8M。性能优化里面比较重要的方面就是内存方面的优化问题,虽然一部手机的内存可能会比较大,但是由于每一个APP进程的内存分配的空间都是有限的,因此性能问题尤其需要注意。
在C/C++中,程序员需要自己去分配内存和释放内存--手动管理,例如malloc free。但是在Java中,内存的管理是交给虚拟机去做的,通过GC来管理内存会有一些不确定性的因素,很多时候是我们主观意愿觉得一个对象应该被回收,但是实际情况不然,这就很可能发生了内存泄漏,关于内存泄漏的知识会在下面讲解。
内存与内存泄露相关知识
java的GC内存回收机制:
某对象不再有任何的引用的时候才会进行回收。
什么是内存泄露:
内存不在GC掌控之内了。当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。
内存分配的几种策略:
1.静态的(生命周期比较长)
静态的存储区:内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在。
它主要存放静态数据、全局的static数据和一些常量。
2.栈式的
在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉。
栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。
3.堆式的
也叫做动态内存分配。有时候可以用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放(java里面直接依赖GC机制)。
在C/C++这里是可以自己掌控内存的,需要有很高的素养来解决内存的问题。java在这一块貌似程序员没有很好的方法自己去解决垃圾内存,需要的是编程的时候就要注意自己良好的编程习惯。
推荐阅读:深入理解JVM虚拟机
栈与堆的区别:
堆是不连续的内存区域,堆空间比较灵活也特别大。
栈式一块连续的内存区域,大小是有操作系统觉决定的。
其中,堆的管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。
对于栈的话,他是一种先进后出的数据结构,进出完全不会产生碎片,运行效率高且稳定。
主要是注意堆的使用,因为栈是JVM管理的
项目应该在一开始的时候就应该养成良好的习惯。
优化:当APP比较卡的时候,如果你不知道在哪里会有内存泄漏的时候,需要使用工具来帮助我们分析代码。常见的有AS自带的工具、MAT
栗子:
public class Main{
int a = 1;//成员变量的基本数据类型都在堆中
Student s = new Student();//成员变量引用以及引用的实体都在堆中
public void XXX(){
int b = 1;//方法里面的局部变量栈里面
Student s2 = new Student();//方法里面的局部变量的引用在栈里面,实体在堆里面
}
}
- 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)---因为他们属于类,类对象最终还是要被new出来的。
- 局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。-----因为他们属于方法当中的变量,生命周期会随着方法一起结束。
我们所讨论内存泄露,主要讨论堆内存,他存放的就是引用指向的对象实体。
有时候确实会有一种情况:当需要的时候可以访问,当不需要的时候可以被回收也可以被暂时保存以备重复使用(缓存起来)。
比如:ListView或者GridView、REcyclerView加载大量数据或者图片的时候,图片非常占用内存,一定要管理好内存,不然很容易内存溢出。滑出去的图片就回收,节省内存。看ListView的源码----回收对象,还会重用ConvertView。如果用户反复滑动或者下面还有同样的图片,就会造成多次重复IO(很耗时),那么需要缓存---平衡好内存大小和IO,算法和一些特殊的java类。
常用的算法:lrucache(最近最少使用先回收)
特殊的java类(引用):使用不同的引用类型有利于回收(不回收)
- StrongReference强引用:
回收时机:从不回收 使用:对象的一般保存 生命周期:JVM停止的时候才会终止 - SoftReference,软引用
回收时机:当内存不足的时候;使用:SoftReference<String>结合ReferenceQueue构造有效期短;生命周期:内存不足时终止 - WeakReference,弱引用
回收时机:在垃圾回收的时候;使用:同软引用; 生命周期:GC后终止 - PhatomReference 虚引用
回收时机:在垃圾回收的时候;使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动; 生命周期:GC后终止
其中,软引用、弱引用用得比较多,虚引用随时都会被回收,一个比一个弱(回收级别)
开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用和弱引用。
但是在Android2.3以后,软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。因此推荐使用LRU算法,例如LruCache。
使用场景:大量的图片显示的时候,
ListView或者GridView、REcyclerView要使用内存缓存+外部缓存(SD卡)
内存泄漏案例
1、静态变量,类加载的时候初始化,类卸载(进程被杀死的时候)释放
public class CommonUtils {
private static CommonUtils sInstance;
private Context mContext;
private CommonUtils(Context context) {
mContext = context;
}
public static CommonUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new CommonUtils(context);
}
return sInstance;
}
}
2、非静态内部类,改为静态内部类,因为不会持有非静态的引用 Thread 或者Timer
方法加上static 或者 自定义静态类继承Thread
延时的,线程的,比Activity的生命周期更加长(归根结底是因为生命周期不一致导致的)
//也是匿名内部类,也会引用外部
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
break;
}
}
};
//非静态(匿名)内部类不正确使用会导致内存泄漏
//已经隐式持有了Activity
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
// LeakActivity.this.a = 10;
a = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
又如:
//可以在Activity 销毁的时候cancel,但是有时候Activity会突然挂掉,不走生命周期,因此还是保持良好的编码习惯
new Timer().schedule(new TimerTask() {
@Override
public void run() {
}
}, 50000);
3、资源没有正确关闭。例如:传感器SensorManager 蓝牙等、广播、自定义控件的时候Attribute资源获取之后没有recycle。
SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);//使用完毕以后应该反注册
Sensor s = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(null, 0);
4、不需要用的监听没有移除,尤其是观察者模式的时候,addXXXListener的时候(因为是添加到集合里面的)
/**
* 这个类需要防止内存泄漏,在ActivityonStop的时候清理所有观察者
*/
public class ListenerController {
//此种Map的特点是:当除了自身有对KEY的引用之外,此Key没有其他引用那么这个Map会丢弃这个值
private static WeakHashMap<View, View.OnClickListener> mListener = new WeakHashMap<>();
public static void setListener(View view, View.OnClickListener listener) {
mListener.put(view, listener);
}
public static void clearListener() {
mListener.clear();
}
}
又如:
final MyView view = new MyView(this);
//监听View的加载,计算宽高,计算完一定要移除。实际上是放到集合里面,add 要注意 set的就算了
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
}
});
5、无限循环动画,在Activity不可见的时候(onStop)应该停止,在onRestart的时候重新开启。
扩展:论Handler的正确使用姿势
典型错误的使用示例:
public class LeakActivity extends AppCompatActivity {
private int a = 10;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
mHandler.sendEmptyMessageDelayed(0, 5000);
}
//也是匿名内部类,也会引用外部
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
a = 20;
break;
}
}
};
}
分析:这是我们用得最多的用法,Handler隐式地引用了Activity(通过变量a)。Handler的生命周期有可能与Activity的生命周期不一致,比如栗子中的sendEmptyMessageDelayed,在5000毫秒之后才发送消息,但是很有可能这时候Activity被返回了,这样会造成Handler比Activity还要长寿,这样会导致Activity发生暂时性的内存泄漏。
姿势一:
为了解决这个问题,我们可以把Handler改为static的,但是这样会造成Handler无法访问Activity的非静态变量a,但是实际开发中我们
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
//a = 20; //不能访问得到
break;
}
}
};
姿势二:
通过把Activity作为Handler成员变量,在Handler构造的时候传进来即可。这时候我们不能使用匿名内部类了,需要把Handler单独抽取成一个类,这样就可以访问Activity的非静态变量了。但是我们的问题又回来了,这时候Handler持有了Activity的强引用了,这样不就是回到我们的原点了吗?(内存泄漏问题依然没有解决)
private static class UIHandler extends Handler {
private LeakActivity mActivity;//外部类的强引用
public UIHandler(LeakActivity activity) {
mActivity = activity;
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mActivity.a = 20;
}
}
姿势三(最终版本):把Activity通过弱引用来作为成员变量。虽然我们把Activity作为弱引用,但是Activity不一定就是会在GC的时候被回收,因为可能还有其他对象引用了Activity。在处理消息的时候就要注意了,当Activity回收或者正在finish的时候,就不能继续处理消息了,再说了,Activity都回收了,Handler还玩个屁!
private static class UIHandler extends Handler {
private WeakReference<LeakActivity> mActivityRef;//GC的时候会回收
public UIHandler(LeakActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//当使用弱引用的时候,会回收Activity吗?
//虽然用的是弱引用,但是并不代表不存在其他的对象没有引用Activity,因此不一定会被回收
//Activity都回收了,Handler还玩个屁!
LeakActivity activity = mActivityRef.get();
if (activity == null || activity.isFinishing()) {
return;
}
mActivityRef.get().a = 20;
}
}
关于更多的Handler使用,请参考我朋友写的文章:
说说Handler的一些使用姿势
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
公众号:Android开发进阶我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。