导言
上一节主要讲了分析内存问题的一些工具,这一节主要是总结一些常见的场景
内存泄漏
说了那么久的内存泄漏,实际上就是对象超过了它本来应该存在的生命周期,导致GC没有成功回收它,在Android中,主要就是Activity,因为Activity有自己的生命周期并且一些页面所占用的内存相当的大,所以说如果在退出之后没有被正常回收,这部分内存泄露还是比较厉害的
静态变量导致的泄露
这个很简单,都知道static修饰的变量会存于方法区中,并且在整个进程运行中不会被GC,所以说一旦static持有了context,那么必须注意泄露的问题
使用static的主要场景就是单例和复用,看一个例子
class LeakSinglton{
companion object {
@Volatile private var INSTANCE:LeakSinglton? = null
fun getInstance(context:Context):LeakSinglton?{
if(null == INSTANCE){
synchronized(LeakSinglton::class){
if(null == INSTANCE){
INSTANCE = indi.fanjh.usekotlin.LeakSinglton(context)
}
}
}
return INSTANCE
}
}
var mContext:Context? = null
private constructor(context:Context){
mContext = context
}
}
其实如果必须要使用单例的话,有一个非常简单的解决方法,那就是使用ApplicationContext,不过此时启动Activity等功能可能会受限,所以说要考虑清楚这个单例是否必要再具体使用,不能只图功能实现
private constructor(context:Context){
mContext = context.applicationContext
}
异步任务导致的泄露等问题
比方说网络请求和一些耗时的操作,常规的处理都是在一个工作线程中处理,然后等待处理完毕再投递回主线程中处理
这会导致两个问题:
1.异步回调的空指针问题,如果你在Activity的onDestroy中手动释放了资源,那么要求你在回调中必须处理
2.内存泄露,当前Activity在执行中已经退出,但是也只能等待耗时任务完成之后,后续的GC才有可能回收该Activity的资源
这类问题的常见解决方案:
1.Handler相关,需要考虑的就是大部分异步任务是通过Handler最后回调会主线程,并且Handler本身也可能执行延时任务
class WeakHandler: Handler {
var weakRef:WeakReference<TestMeasureActivity>? = null
constructor(testMeasureActivity: TestMeasureActivity){
weakRef = WeakReference(testMeasureActivity)
}
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
val activity = weakRef!!.get()
if(null != activity){
//说明当前Activity还不应该被回收,可以继续操作,do something
}
}
}
回调方面的处理就是通过弱引用来持有Activity,因为弱引用持有的对象在只有弱引用持有的情况下,GC也是会进行回收的,此时get()获得的就是null对象
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
在对应的Activity的onDestroy中移除handler中所有未执行的回调和消息,这样也可以有效避免延时任务的再执行
2.Thread类型,这个和Handler类似,需要注意的就是后台任务需要在合适的时机进行终止,避免其肆意执行
比方说AsyncTask
override fun onDestroy() {
super.onDestroy()
asyncTask!!.cancel(true)
}
实际上就是在onDestroy中终止,并且尝试中断当前线程
对于普通的线程来说,也是一样的处理,比方说
class ThreadDemoActivity: Activity(){
companion object {
const val TAG = "ThreadDemoActivity"
}
var mThread:CalculateThread? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mThread = CalculateThread()
mThread!!.start()
}
override fun onDestroy() {
super.onDestroy()
if(mThread != null){
mThread!!.isCancelled = true
//如果你认为应该强势中断线程,那么你可以
var isSuccess = false
try {
mThread!!.interrupt()
isSuccess = true
}finally {
Log.d(TAG,"interrupt_result:$isSuccess")
}
}
}
class CalculateThread:Thread(){
var isCancelled = false
override fun run() {
super.run()
//耗时操作一
if(isCancelled){
return
}
//耗时操作二
if(isCancelled){
return
}
//后续普通操作
}
}
}
换言之,线程有自然终止和强行中断两种操作,这里要根据具体的场景自行选择使用
注册和反注册问题
这个实际上也是长久持有的问题,比方说EventBus就是static持有,常见于观察者模式下的单例,广播是因为系统服务长期存在的原因,总之就是注意注册了要反注册,要成对存在
资源释放
这个指的就是一些持有大量资源的类的释放,比方说IO流和Cursor,这两个最常见
图片处理
这里实际上就是说的Bitmap的处理,从上一节当中也可以看到,实际上图片内存占用是非常大,所以说合适的图片处理也是很重要的
图片选择问题
考虑图片加载的使用场景,比方说同样是华为手机,但是一个屏幕密度高,一个屏幕密度低,此时实际上展示在手机上面的同一幅图像素实际上是不一样的,此时简单的方案是直接使用一张大图,然后通过图片加载库进行压缩等处理展示
比方说480x480,处理为160x160和240x240两种,但是很明显的,最合适的做法应该是根据屏幕密度来下发具体的图片,我们知道网络图片的加载地址来源于服务端,那么如果我们在请求接口的时候带上当前要请求的图片密度,然后区别返回xxx/v1/demo.jpg或者xxx/v2/demo.jpg,并且图片的大小和实际展示的像素大小一致,这样是最好的选择
上述相对于一个源图片来说,实际上就是解决了两个问题
1.不同密度的图片本身大小就不同,那么低密度的手机在进行网络请求请求图片的时候,这个过程消耗的流量会少一些,并且加载的速度也会快一些
2.图片的尺寸完全匹配,那么将会减少多余的压缩等处理,也会加快图片的展示速度
图片质量问题
先看图片格式选择
现在市场上常用的图片有jpg、png以及webp,webp的介绍可以看下面这个链接
https://zhuanlan.zhihu.com/p/23648251
下面稍微介绍一下三者的区别:
jpg:有损压缩,并且没有Alpha通道,相对于png来说会小一点,非常适合一些没有Alpha通道的大图
png:无损压缩,支持Alpha通道,一般在追求图片质量的时候可以考虑,适用于一些小图
webp:Google出品,支持有损和无损压缩两种模式,并且图片大小会小于png和jpg,缺点就是Android原生在4.0之后才支持,并且透明的在4.2.1之后才会完美支持,并且webp动图目前只有Fresco支持
图片像素质量
每一个像素点需要存储在byte[]中,那么每一个像素点的存储方式也会影响到图片的大小
/**
* Possible bitmap configurations. A bitmap configuration describes
* how pixels are stored. This affects the quality (color depth) as
* well as the ability to display transparent/translucent colors.
*/
public enum Config {
// these native values must match up with the enum in SkBitmap.h
/**
* Each pixel is stored as a single translucency (alpha) channel.
* This is very useful to efficiently store masks for instance.
* No color information is stored.
* With this configuration, each pixel requires 1 byte of memory.
*/
ALPHA_8 (1),
/**
* Each pixel is stored on 2 bytes and only the RGB channels are
* encoded: red is stored with 5 bits of precision (32 possible
* values), green is stored with 6 bits of precision (64 possible
* values) and blue is stored with 5 bits of precision.
*
* This configuration can produce slight visual artifacts depending
* on the configuration of the source. For instance, without
* dithering, the result might show a greenish tint. To get better
* results dithering should be applied.
*
* This configuration may be useful when using opaque bitmaps
* that do not require high color fidelity.
*/
RGB_565 (3),
/**
* Each pixel is stored on 2 bytes. The three RGB color channels
* and the alpha channel (translucency) are stored with a 4 bits
* precision (16 possible values.)
*
* This configuration is mostly useful if the application needs
* to store translucency information but also needs to save
* memory.
*
* It is recommended to use {@link #ARGB_8888} instead of this
* configuration.
*
* Note: as of {@link android.os.Build.VERSION_CODES#KITKAT},
* any bitmap created with this configuration will be created
* using {@link #ARGB_8888} instead.
*
* @deprecated Because of the poor quality of this configuration,
* it is advised to use {@link #ARGB_8888} instead.
*/
@Deprecated
ARGB_4444 (4),
/**
* Each pixel is stored on 4 bytes. Each channel (RGB and alpha
* for translucency) is stored with 8 bits of precision (256
* possible values.)
*
* This configuration is very flexible and offers the best
* quality. It should be used whenever possible.
*/
ARGB_8888 (5),
/**
* Each pixels is stored on 8 bytes. Each channel (RGB and alpha
* for translucency) is stored as a
* {@link android.util.Half half-precision floating point value}.
*
* This configuration is particularly suited for wide-gamut and
* HDR content.
*/
RGBA_F16 (6),
/**
* Special configuration, when bitmap is stored only in graphic memory.
* Bitmaps in this configuration are always immutable.
*
* It is optimal for cases, when the only operation with the bitmap is to draw it on a
* screen.
*/
HARDWARE (7);
final int nativeInt;
private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE
};
Config(int ni) {
this.nativeInt = ni;
}
static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}
实际上关注RGB_565和ARGB_8888即可,在没有Alpha通道的时候,使用RGB_565对于内存来说是最好的选择,对比于ARGB_8888来说,每一个像素点可以接受2个字节的大小,对于一张图片来说60 * 60 * 2都可以节省下7KB左右的大小,在网络图片为jpg的时候这个为最佳选择。
对象分配
个人总结其实就几个方面:
1.尽量懒加载,特别是static变量,使用的地方再初始化
2.POJO中的属性,boolean或者int类型尽量不要用String修饰,这样会导致在堆中分配很多多余对象,int则可以做到尽可能的复用栈中数据
3.String类型操作,如果有大量拼接操作请使用StringBuilder,实际上+号拼接多个对象的时候也是new StringBuilder实现,所以说特别在for循环当中,能够在外部定义一个StringBuilder对象处理最优
StrictMode
Android提供了工具进行上述的一些方面的自动检查,使用也很简单,比方说在Application的onCreate中处理
class MainApplication:Application(){
override fun onCreate() {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build())
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
super.onCreate()
}
StrictMode主要可以作以下方面的检查:
1.IO读写操作是否在工作线程中进行
2.网络请求是否在工作线程中进行
3.自定义标记线程,可以进行慢线程操作的检查
4.Closeable对象是否close
5.SQLiteDatabase及Cursor是否close
6.Activity泄露
7.BroadcastReceiver及ServiceConnection导致的Context泄露,主要就是注册后是否反注册,ServiceConnection因为bindService是通过Context作为key缓存的,所以说也要注意unBindService
。。。
上述的这些检查项目是允许开发者自己选择的,从我个人的角度来说,建议全部开启,毕竟养成良好的编程习惯要从平时做起
检查到问题之后,StrictMode惩罚模式也有打印日志和直接奔溃等几种行为,推荐通过打印日志的方式来具体定位问题并且进行修改
结论
这一节主要是一些经验之谈,关于内存方面的,这一块需要通过大量的项目实践来慢慢感受,但是有一点是确认的,良好的编程习惯应该从平时养成,开发者应该对自己的代码有这比较高的要求才行
文章系列:
基本的优化总结(一)
基本的优化总结(二)
基本的优化总结(三)
基本的优化总结(四)
基本的优化总结(五)
基本的优化总结(六)
基本的优化总结(七)