第八章 性能优化

文章目录

  • 内存优化
    • ANR & CRASH 产生的原因是什么?如何解决?
    • 内存溢出 & 内存泄漏 & 内存抖动 是什么?产生原因?解决方案?
    • Bitmap优化 原因 & 方案?
    • 谈谈你项目中内存优化的一些经验?
  • 启动优化
    • 什么是冷启动 & 热启动?启动流程?如何优化启动?
  • 布局优化
    • 你知道哪些布局优化的方案?

内存优化

Carson_Ho:Android性能优化:这是一份全面&详细的内存优化指南

ANR & CRASH 产生的原因是什么?如何解决?

定义 原因 解决
ANR application not response,应用程序的UI线程响应超时 一般是主线程未及时响应用户的输入事件(如触摸、按键);或者当前的事件正在被处理,但是由于耗时太长没有能够及时完成
常见:主线程频繁进行耗时操作
使用多线程,将耗时操作交给工作线程执行
Crash 应用程序崩溃 引起应用程序崩溃的很多原因时因为内存溢出OOM,因此需要避免OOM现象 内存优化,如:
1. 避免内存泄露
2. 避免内存抖动
3. 图片Bitmap优化
4. 提高代码质量 & 减少代码数量

内存溢出 & 内存泄漏 & 内存抖动 是什么?产生原因?解决方案?

  1. 内存泄露
  • 定义
    当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
  • 本质原因
    持有引用的对象的生命周期>被引用的对象的生命周期
  • 内存泄露 原因 & 解决方案
    (1)集合类
  • 原因
    集合中添加对象时,集合会存储着该对象的引用。导致该对象不可被回收,从而引起内存泄露。
public void example(){
	ArrayList<Object> arr = new ArrayList<>();
	for(int i=0;i<10;i++){
		Object obj = new Object();
		arr.add(obj);	// arr中存储obj的引用(在栈内存中的地址)
		obj = null;		// 虽释放元素obj本身,但由于arr中仍持有obj的引用,导致GC仍无法回收obj对象,引起内存泄露
	}
}

第八章 性能优化_第1张图片

  • 解决
    集合类 添加集合元素对象 后,在使用后必须从集合中删除
// 清空集合对象 & 设置为null
arr.clear();
arr = null;

(2)Static关键字修饰成员变量

  • 原因
    由于Static关键字修饰的成员变量的生命周期 = 应用程序的生命周期。若Static关键字所引用实例 < 应用程序的生命周期时,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露。
// 单例模式
// 由于单例模式中对象由于其静态特性,因此单例模式引用对象的生命周期 = 应用程序生命周期
// 则若单例模式持有一个 生命周期小于应用生命周期 的对象引用,则当该实例对象被销毁时,由于单例对象仍持有该对象的引用,导致该对象无法销毁,引起内存泄露
public class SingleInstance{
	public static SingleInstance instance;
	public Context context;
	
	public SingleInstance(context){
		this.context = context;
	}

	public SingleInstance getInstance(Context context){
		if(instance == null)
			synchronize(this)
				if(instance == null)
					instance = new SingleInstance(context);
	// 若传入的context 是 activity 的 context
	// 则当该activiy生命周期结束被销毁时,由于单例模式的instance扔持有该activity的引用,导致activity无法被销毁,引起内存泄露
		return instance;
	}
}
  • 解决
  • 尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context)。若需引用 Context,则尽量使用Applicaiton的Context
  • 使用 弱引用(WeakReference) 代替 强引用 持有实例
	public SingleInstance(context){
	// 单例模式的context 应该为应用的context(ApplicationContext)
		this.context = context.getApplicationContext();
	}

(3)非静态内部类/匿名类
菜鸟教程:Java 内部类详解
非静态内部类 / 匿名类 默认持有 外部类的引用:因为非静态内部类依赖外部类,可以通过内部类对外部类的引用来访问外部类的成员变量和成员方法。
而静态内部类则不持有外部类的引用:静态内部类不依赖外部类。
(3.1)多线程:AsyncTask、实现Runnable接口、继承Thread类

  • 原因
    多线程类为非静态内部类/匿名类,实例化后默认持有外部类的引用。因此当工作线程正在处理任务时,当Activity被销毁时,由于工作线程持有外部类Activity的引用,导致Activity无法被垃圾回收器回收,从而造成内存泄露。
public class MainActivity extends Activity{
	@override
	public void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		MyThread t = new MyThread();	// 创建多线程实例new MyThread(),此时t默认持有外部类MainActivity的引用
		t.start();	// 开启线程
	}
	// 创建Thread内部类,实现多线程
	public class MyThread extends Thread{
		@override
		public void run(){
			try{
				// 若线程执行的5秒内,MainActivity被销毁,但由于工作线程持有外部类的引用,因此MainActivity无法被GC回收,会造成内存泄露
				Thread.sleep(5000);
			}catch(InterruptedException e){e.printStackTrace();}
		}
	}
}
  • 解决
    (1)将非静态内部类 设置为 静态内部类
    静态内部类 不默认持有外部类的引用,因此工作线程不持有MainActivity的引用
public static class MyThread extends Thread{

(2)当外部类结束生命周期时,强制结束线程

@override
public void onDestroy(){
	super.onDestroy();
	thread.stop();	// 外部类Activity生命周期结束时,强制结束线程
}

(3.2)消息传递机制Handler

  • 原因
    因为消息队列中的Message持有Handler实例的引用,Handler实例为 非静态内部类/匿名类 持有外部类Activity的引用。即Message->Handler->Activity。
    因此当Handler 消息队列中仍有未处理的消息/正在处理消息时,若外部类MainActivity销毁(Handler 生命周期 > Activity 生命周期),由于Activity被引用,因此GC无法回收MainActivity导致内存泄露
  • 解决
    将Handler子类设置为静态内部类 ,则Handler不会引用MainActivity实例
    使用WeakReference弱引用持有Activity实例,则垃圾回收期进行扫描时,只要发现了具有弱引用的对象,便会回收它的内存
public class MainActivity extends Activity{
	@override
	protected void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		// 传入当前Activity实例(弱引用)
		MyHandler mh = new MyHandler(this);
	}
	// 1. 自定义Handler子类,设置为静态内部类
	public static class MyHandler extends Handler{
		// 2. 定义弱引用实例
		private WeakReference<Activity> reference;
		public MyHandler(Activity activity){
			refrence = new WeakReference<Activity>(activity);
		}
		@override
		public void handleMessage(Message msg){
			System.out.println(msg.obj);
		}
	}
}

(4)资源对象使用后未关闭

  • 原因
    对于资源的使用(如 广播BraodcastReceiver、文件流File、数据库游标Cursor、图片资源Bitmap等),若在Activity销毁时无及时关闭 / 注销这些资源,则这些资源将不会被回收,从而造成内存泄漏
  • 解决
    在Activity销毁时 及时关闭 / 注销资源
// 对于 广播BraodcastReceiver:注销注册
unregisterReceiver()

// 对于 文件流File:关闭流
InputStream / OutputStream.close()

// 对于数据库游标cursor:使用后关闭游标
cursor.close()

// 对于 图片资源Bitmap:Android分配给图片的内存只有8M,若1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null 
Bitmap.recycle();
Bitmap = null;

// 对于动画(属性动画)
// 将动画设置成无限循环播放repeatCount = “infinite”后
// 在Activity退出时记得停止动画

(5)其他

  • Context
    Context的生命周期大于Context所引用实例的生命周期时,会造成内存泄露。应该尽量使用ApplicationContext代替ActivityContext。
  • ListView
    在滑动ListView获取最新的View时,每次都在getView()中重新实例化一个View对象,不仅浪费资源、时间,也使内存占用越来越大。导致内存泄露。
    第八章 性能优化_第2张图片
  1. 内存溢出 & 内存泄露 & 内存抖动 对比
定义 原因 解决
内存溢出 应用程序所需内存超出系统分配的内存限额,从而导致内存溢出 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
内存泄露
代码中存在死循环或循环产生过多重复的对象实体(内存抖动)
使用的第三方软件中的BUG
启动参数内存值设定的过小
内存泄露 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏 持有引用者的生命周期>被引用者的生命周期:
集合类
Static关键字修饰成员变量
非静态内部类/匿名类
资源对象使用后未关闭
集合类:回收集合元素
Static关键字修饰的成员变量:避免Static引用过多实例
非静态内部类/匿名类:使用静态内部类
资源使用后未关闭:关闭资源对象
内存抖动 内存大小不断浮动的现象 由于大量、临时的小对象频繁创建,导致程序频繁地分配内存 & 垃圾回收器(GC)频繁回收内存
垃圾收集器(GC)频繁地回收内存会导致卡顿,甚至内存溢出(OOM)——大量、临时的小对象频繁创建会导致内存碎片,使得当需分配内存时,虽总体上有剩余内存可分配,但由于这些内存不连续,导致无法模块分配。系统则视为内存不够,故导致内存溢出OOM
尽量避免频繁创建大量、临时的小对象
  1. 辅助分析内存泄露的工具
  • MAT(Memory Analysis Tools)
  • Heap Viewer
  • Allocation Tracker
  • Memory Monitor(Android Studio 自带 的图形化检测内存工具,用于跟踪系统 / 应用的内存使用情况)
  • LeakCanary

Bitmap优化 原因 & 方案?

  • 原因
    图片资源(Bitmap)非常消耗内存,占用App内存大部分。Android系统分配给每个应用程序内存有限,因此可能引发内存溢出(OOM),导致应用崩溃(Crash)
  • 方案
  1. 使用完毕后释放图片资源
// 方案1:采用软引用
reference = new SoftReference<Bitmap>(bm);
// 方案2:Bitmap像素数据回收
bm.recycle();
  1. 根据分辨率适配 & 缩放图片

public void decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight){
	// 1. 加载图片前获取图片实际长宽值
	BitmapFactory.Options options = new BitmapFactory.Options();
	options.inJustDecodeBounds = true;	// 禁止为bitmap分配内存
	BitmapFactory.decodeResource(res,resId,options);	//	对位图进行解析
	// 2. 计算图片的压缩比inSampleSize,对图片进行压缩
	options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
	// 3. 用获取到的inSampleSzie再次解析图片,可获得压缩后的图片
	options.inJustDecodeBounds = false;	// 在解析图片后创建Bitmap对象并为图片分配内存
	return Bitmap.decodeResource(res,resId,options);
}

// 2. 根据图片控件的宽/高对图片大小进行适配——计算对应的缩放比inSampleSize=实际宽高/目标宽高
public int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
	int imageHeight = options.outHeight;
	int imageWidth = options.outWidth;
	int inSampleSize = 1;
	if(imageHeight > reqHeight || imageWidth>reqWidth){
		final int heightRatio = Math.round((float)imageHeight/(float)reqHeight);
		final int widthRatio = Math.round((float)imageWidth/(float)reqWidth);
		inSampleSize = heightRatio < widthRatio?heightRatio : widthRatio;
	}
	return inSampleSize
}
  1. 按需选择合适的解码方式
    第八章 性能优化_第3张图片
    不同的图片解码方式 对应的 内存占用大小 相差很大。根据需求通过 BitmapFactory.inPreferredConfig 设置 合适的解码方式。(默认使用的解码方式:ARGB_8888)
  2. 设置图片缓存——三级缓存机制
     /**
      * 从缓存(内存缓存,磁盘缓存)中获取Bitmap
      */
     @Override
     public Bitmap getBitmap(String url) {
         if (mLruCache.get(url) != null) {
             // 从LruCache缓存中取
             Log.i(TAG,"从LruCahce获取");
             return mLruCache.get(url);
         } else {
             String key = MD5Utils.md5(url);
             try {
                 if (mDiskLruCache.get(key) != null) {
                     // 从DiskLruCahce取
                     Snapshot snapshot = mDiskLruCache.get(key);
                     Bitmap bitmap = null;
                     if (snapshot != null) {
                         bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0));
                 }
                 else{
					bitmap = HttpUtils.getImageFromNet(url);
				}
             // 存入缓存
             putBitmap(url, bitmap);
             return bitmap;
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         return null;
     }
 
     /**
      * 存入缓存(内存缓存,磁盘缓存)
      */
     @Override
     public void putBitmap(String url, Bitmap bitmap) {
         // 存入LruCache缓存
         mLruCache.put(url, bitmap);
         // 判断是否存在DiskLruCache缓存,若没有存入
         String key = MD5Utils.md5(url);
         try {
             if (mDiskLruCache.get(key) == null) {
                 DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                 if (editor != null) {
                     OutputStream outputStream = editor.newOutputStream(0);
                     if (bitmap.compress(CompressFormat.JPEG, 100, outputStream)) {
                         editor.commit();
                     } else {
                         editor.abort();
                     }
                 }
                mDiskLruCache.flush();
             }
         } catch (IOException e) {
             e.printStackTrace();
         }
 
     }

谈谈你项目中内存优化的一些经验?

第八章 性能优化_第4张图片

启动优化

什么是冷启动 & 热启动?启动流程?如何优化启动?

  • 冷启动 & 热启动
方式 冷启动 热启动
定义 启动应用时,后台没有该应用的进程(例:第一次开启应用,上一次彻底退出应用),这时系统会重新创建一个新的进程分配给该应用,这种启动方式就是冷启动 启动应用时,后台已有该应用的进程(例:按back,home键,应用退出,但仍保留在后台,可进入任务列表查看),从已有的进程中启动应用,这种启动方式就是热启动
特点 系统会重新创建一个新进程分配给它。
因此会先创建和初始化Application类,再创建和初始化MainActivity类,包括一系列测量布局绘制,最后显示在界面上
系统直接从已有进程中启动应用。
因此不必创建和初始化Application,直接创建和初始化MainActivity,包括一系列测量不聚会知,显示在界面上
流程 Zygote进程中fork创建出一个新的进程 –> Application构造器 –> attachBaseContext() –> onCreate() –> Activity构造器 –> onCreate –> 配置主题背景等属性 –> onStart() –> onResume –> 测量布局绘制显示在界面上 (没有Application创建和初始化)Activity构造器 –> onCreate –> 配置主题背景等属性 –> onStart() –> onResume –> 测量布局绘制显示在界面上
  • 优化启动方案
  • 黑白屏优化
    系统在启动Activity的setContentView之前绘制窗体,此时布局资源还未加载,于是使用了默认的背景色。
    解决:把启动图bg_splash设置为窗体背景,避免刚刚启动App的时候出现,黑/白屏
    <style name="Theme.AppLauncher" parent="@android:style/Theme.NoTitleBar.Fullscreen">
        "android:windowBackground">@drawable/bg_splash
    style>

配置启动页面SplashActivity的清单文件

    <activity android:name="tv.douyu.view.activity.SplashActivity"
        android:screenOrientation="portrait" android:theme="@style/Theme.AppLauncher">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        intent-filter>
    activity>

  • onCreate优化
    onCreate()耗时长会影响应用程序布局绘制的时间。因此应该减少onCreate工作量。
    一般重写Application,在onCreate()方法中做一些初始化操作(如第三方SDK配置),可以将这些较大的第三方库通过开启一个异步线程中进行初始化。

布局优化

你知道哪些布局优化的方案?

Android性能优化之布局优化

  • 布局优化思想
    减少Overdraw(过度绘制)(一般通过减少UI层级、简化布局实现)

Overdraw:描述的是屏幕上的某个像素在同一帧时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,浪费大量的CPU以及GPU资源。

  • 布局优化方法
  1. 善用相对布局RelativeLayout
    可以通过扁平的RelativeLayout降低LinearLayout嵌套所产生布局树的层级
  2. 使用抽象布局标签include、merge、ViewStub
    include、merge、ViewStub标签?
  • < include />
    include标签常用于将布局中的公共部分提取出来
  • < merge />
    merge标签是作为include标签的一种辅助扩展来使用,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套(merge能够减少include可能产生的层级)
    直接使用include标签引入了之前的LinearLayout之后导致了界面多了一个层级,若引入merge标签则可以减少一个层级
  • < ViewStub />
    ViewStub是View的子类。他是一个轻量级View, 隐藏的,没有尺寸的View。他可以用来在程序运行时简单的填充布局文件
  1. 使用Android最新的布局方式ConstaintLayout  
    ConstraintLayout允许你在不适用任何嵌套的情况下创建大型而又复杂的布局。它与RelativeLayout非常相似,所有的view都依赖于兄弟控件和父控件的相对关系。但是,ConstraintLayout比RelativeLayout更加灵活

你可能感兴趣的:(Android面试之旅)