第十五章-Android性能优化

推荐个音乐下载app【佳语音乐下载】
https://gitlab.com/gaopinqiang/checkversion/raw/master/Music_Download.apk

第十四章-JNI和NDK编程
这个我在之前的博客中有写过,书中也只是简单的介绍了下开发的流程。所以可以参考之前的博客就行。
https://blog.csdn.net/gaopinqiang/article/details/76652575

本章是最后一章,介绍的主题是Android的性能优化方法和程序设计的一些思想。
一、Android的性能优化方法

本节主要介绍一些有效的性能优化方法,主要内容包括布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView优化、Bitmap优化、线程优化以及一些性能优化建议。

1、布局优化

布局优化的思路很简单,就是尽量减少布局文件的层级,布局层级少了,这就意味着Android绘制时的工作量少了,那么程序的性能自然就高了。

如何进行布局优化呢?首先删除布局中的无用控件和层级,其次有选择地使用性能较低的ViewGroup,比如RelativeLayout,如果布局中既可以使用LinearLayout,也可以使用RelativeLayout,那就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局需要花费更多的CPU时间。FrameLayout和LinearLayout一样都是一种简单高效的ViewGroup,因此可以考虑使用它们。

布局优化还有另外一种手段是采用标签、标签和ViewStub。标签主要用于布局重用,标签一般和配合使用,它可以降低减少布局的层级,而ViewStub则提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内存,这提高了程序的初始化效率。

< include>标签

<include>标签可以将一个指定的布局文件加载到当前的布局文件中,如下所示。
	<?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">

		<include layout="@layout/titlebar"/>
		
	</LinearLayout>

上面的代码中,@layout/titlebar指定了另外一个布局文件,通过这种方式就不用把titlebar这个布局的内容再重复写一遍了,这就是的好处。标签只支持android:layout开头的属性,比如android:layout_width、android:layout_height,其他的属性是不支持的,比如android:backgroup。当然android:id这个属性是个特例,如果指定了这个id属性,同时被包含的布局文件的根元素也指定了id属性,那么以指定的id属性为准。需要注意的是,如果标签指定了android:layout_*这种属性,那么android:layout_width和android:layout_height是必须存在的。

< merge>标签
< merge>标签一般和一起使用从而减少布局的层级。比如上面的布局是一个竖直方向的LinearLayout,这个时候如果被包含的布局文件中也采用了竖直方向的LinearLayout,那么显然被包含的布局文件中的LinearLayout是多余的,通过标签就可以去掉多余的那一层LinearLayout。

	<?xml version="1.0" encoding="utf-8"?>
	<merge xmlns:android="http://schemas.android.com/apk/res/android"
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:orientation="vertical">

		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:text="test" />

	</merge>

ViewStub
ViewStub继承View,它非常轻量级且高宽都是0,因此它本身不参与任何的布局和绘制过程。ViewStub的意义在于按需加载所需的布局文件,在实际的开发中,很多布局文件在正常的情况下不会显示,比如网络异常时的界面,这个时候就没有必要在整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用的时候再加载,提高了程序初始化时的性能。

    <ViewStub
		android:id="@+id/stub_import"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/include_main_test"
        android:inflatedId="@id/ll_include"
        />

其中stub_import是ViewStub的id。如何做到按需加载呢?可以按照下面的两种方式:

	((ViewStub)findViewById(R.id.stub_import).setVisibility(View.VISIBLE);
或者	
	((ViewStub)findViewById(R.id.stub_import)).inflate()

当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,这个时候ViewStub就不再是整个布局结构的一部分了。目前ViewStub还不支持< merge >标签。

2、绘制优化
绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面。

首先,onDraw中不要创建新的局部对象,这是因为onDraw方法可能会被频繁调用,这样就会瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。

另外一方面,onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管循环是很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。按照Google官方给出的性能优化典范中的标准,View的绘制帧率保证60fps是最佳的,这就要求绘制时间不能超过16ms(16ms = 1000/60)。

3、内存泄漏优化

内存泄漏在开发过程中是一个需要重视的问题,但是由于内存泄漏问题对于开发者的经验和开发意识有较高的要求,因此这也是开发人员最容易犯的错误之一。内存泄漏的优化分两个方面,一方面是在开发过程中避免写出内存泄露的代码,另一方面是通过一些分析工具比如MAT来找出潜在的内存泄漏继而解决。下面介绍几个常见的内存泄漏的场景。

场景1:静态变量导致的内存泄漏
下面这种情形是最简单的内存泄漏,相信大家都不会这么干,下面代码将导致Activity无法正常销毁,因为静态变量sContext引用了它。

	public class MainActivity extends AppCompatActivity {
		private static final String TAG = "MainActivity";
		
		private static Context sContext;

		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);
			sContext = this;//将activity对象赋值给sContext这个静态变量,导致内存泄漏
		}
	}

上面的代码也可以改造一下。sView是一个静态变量,它内部持有了当前的Activity,所以Activity仍然无法释放

	public class MainActivity extends AppCompatActivity {

		private static final String TAG = "MainActivity";
		//内存泄漏
		private static View sView;
		
		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);
			sView = new View(this);
		}
	}

场景2:单例模式导致的内存泄漏
静态变量导致的内存泄漏都太过于明显,相信大家都不会犯这种错误,而单例模式带来的内存泄漏是我们很容易忽视的,如下所示。首先提供一个单例模式的TestManager,TestManager可以接收外部的注册并将外部的监听器存储起来。

	public class TestManager {
		private List<OnDataArrivedListener> mOnDataArrivedListener = new ArrayList<OnDataArrivedListener>();

		private static class SingletonHolder {
			public static final TestManager INSTANCE = new TestManager();
		}

		private TestManager() {
		}

		public static TestManager getInstance() {
			return SingletonHolder.INSTANCE;
		}

		public synchronized void registerListener(OnDataArrivedListener listener) {
			if (mOnDataArrivedListener.contains(listener)) {
				mOnDataArrivedListener.add(listener);
			}
		}

		public synchronized void unregisterListener(OnDataArrivedListener listener) {
			mOnDataArrivedListener.remove(listener);
		}

		public interface OnDataArrivedListener {
			public void onDataArrived(Object data);
		}
	}

接着再让Activity实现OnDataArrivedListener接口并向TestManager注册监听,如下所示。下面的代码由于缺少解注册的操作会引起内存泄漏,泄漏的原因是Activity的对象被单例模式的TestManager所持有,而单例模式的特点是生命周期和Application保持一致,所以Activity无法被及时释放。

	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		TestManager.getInstance().registerListener(this);
	}

场景3:属性动画所导致的内存泄漏
从Android3.0开始,Google提供了属性动画,属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终Activity无法释放。下面的动动画是无限动画,会泄露当前Activity,解决方法是在 Activity的 onDestroy中调用 animator.cancel()来停止动画。

	public class MainActivity extends AppCompatActivity  {
		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);

			mButton = (Button) findViewById(R.id.mButton);
			ObjectAnimator animator = ObjectAnimator.ofFloat(mButton,"rotation",0,360).setDuration(2000);
			animator.setRepeatCount(ValueAnimator.INFINITE);
			animator.start();
			//animator.cancel();
		}
	}

4、响应速度优化和ANR日志分析
响应速度优化的核心思想是避免在主线程中做耗时操作,但是有时候的确有很多耗时操作,怎么办呢?可以将这些耗时的操作放在线程中去执行,即采用异步的方式执行耗时操作。
响应速度过慢更多地体现在Activity的启动速度上面,如果在主线程中做太多事情会导致Activity启动时出现黑屏现象,甚至出现ANR。Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR。BroadcastReceiver是10秒。系统会在 data/anr目录下创建一个文件 traces.txt,通过分析这个文件就能定位出ANR的原因。下面模拟一个ANR的场景。下面的代码在 Activity的 oncreate中休眠30s,程序运行后持续点击屏幕,应用一定会出现ANR:

	public class MainActivity extends AppCompatActivity  {

		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);

			SystemClock.sleep(30 * 1000);
		}
	}

trace.txt文件在/data/anr/traces.txt中。

如果在Android10上面没有权限导出traces.txt可以使用bugreport命令(会成成比较完整的日志,本地测试有14M左右)
【adb bugreport anrlog.zip】

具体的traces文件的内容分析也比较简单,会有明确的提示。例如:

	"main" prio=5 tid=1 Blocked
	  | group="main" sCount=1 dsCount=0 flags=1 obj=0x74e80d60 self=0xef579000
	  | sysTid=27326 nice=-4 cgrp=default sched=0/0 handle=0xf708e494
	  | state=S schedstat=( 3483871367 187630217 2232 ) utm=313 stm=34 core=5 HZ=100
	  | stack=0xff2e1000-0xff2e3000 stackSize=8MB
	  | held mutexes=
	  at android.app.Activity.finish(Activity.java:5873)
	  - waiting to lock <0x0b4b107d> (a com.example.basetest.MainActivity) held by thread 14
	  at android.app.Activity.finish(Activity.java:5907)
	  at com.example.basetest.MainActivity.endDisturbAnalysisTest(MainActivity.java:2552)
	  at com.example.basetest.MainActivity.onBackPressed(MainActivity.java:2499)
	  at android.app.Activity.onKeyUp(Activity.java:3229)
	  at android.view.KeyEvent.dispatch(KeyEvent.java:2792)
	  at android.app.Activity.dispatchKeyEvent(Activity.java:3537)
	  at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:403)
	  at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:5661)

很明显看到在等待0x0b4b107d这个锁,这个锁被thread 14持有,我们看下这个线程的状态就明白了。

5、ListView和Bitmap优化
ListView在12章就已经做了介绍,这里简单回顾一下。主要分为三个方面:首先采用ViewHolder并避免在getView中执行耗时操作;其次是根据列表的滑动状态来控制任务的执行频率,比如列表快速滑动的时候显然不太适合开启大量的异步任务的;最后可以尝试开启硬件加速来使Listview滑动更加流畅。

Bitmap优化同样在12章已经做了详细的介绍,主要通过BitmapFactory.Options来根据需要对图片进行采样,采样过程主要用到了BitmapFactory.Options的inSampleSize参数。

6、线程优化
线程优化的思想是采用线程池,避免程序中存在大量的Thread。线程池可以重用内部的线程,从而避免了线程的创建和销毁所带来的性能开销。

7、一些性能建议

  • 避免创建过多的对象;
  • 不要过多使用枚举,枚举占用的内存空间要比整型大;
  • 常量请使用static final来修饰;
  • 使用一些Android特有的数据结构,比如SparseArray和Pair等,它们都具有更好的性能;
  • 适当使用软引用和弱引用;
  • 采用内存缓存和磁盘缓存;
  • 尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄漏。

二、内存泄漏分析之MAT工具
MAT的全称Eclipse Memory Analyzer,它是一款强大的内存泄漏分析工具,MAT不需要安装,下载解压即可使用。

关于MAT的使用之前有写过博客;
https://blog.csdn.net/gaopinqiang/article/details/78640454

三、提高程序的可维护性
本节所讲述的内容是Android的程序设计思想,主旨是如何提高代码的可维护性和可扩展性,而程序的可维护性本质上也包含可扩展性。本节的切入点为:代码风格、代码的层次性和单一职责原则、面向扩展编程程以及设计模式,下面围绕着它们分别展开。

可读性是代码可维护性的前提,一段别人很难读懂的代码的可维护性显然是极差的。而良好的代码风格在一定程度上可以提高程序的可读性。代码风格包含很多方面,比如命名规范、代码的排版以及是否写注释等。到底什么样的代码风格是良好的?这是个仁者见仁的问题,下面是笔者的一些看法。

  • (1)命名要规范,要能正确地传达出变量或者方法的含义,少用缩写,关于变量的前缀可以参考Android源码的命名名方式,比如私有成员以m开头,静态成员以s开头,常量则全部用大写字母表示,等等。
  • (2)代码的排版上需要留出合理的空白来区分不同的代码块,其中同类变量的声明要放在一组,两类变量之间要留出一行空白作为区分。
  • (3)仅为非常关键的代码添加注释,其它地方不写注释,这就对变量和方法的命名风格提出了很高的要求,一个合理的命名风格可以让读者阅读源码就像在阅读注释一样,因此根本不需要为代码额外写注释。

代码的层次性是指代码要有分层的概念,对于一段业务逻辑,不要试图在一个方法或者一个类中去全部实现,而要将它分成几个子逻辑,然后每个子逻辑做自己的事情,这样既显得代码层次分明,又可以分解任务从而实现简化逻辑的效果。

单一职责是和层次性相关联的,代码分层以后,每一层仅仅关注少量的逻辑,这样就做到了单一职责。

程序的扩展性标志着开发人员是否有足够的经验,很多时候在开发过程中我们无法保证已经做好的需求不在后面的版本发生变更,因此在写程序的过程中要时刻考虑到扩展,考虑着如果这个逻辑后面发生了改变那么需要做哪些修改,以及怎么样才能降低修改的工作量,面向扩展编程会使程序具有很好的扩展性。

恰当地使用设计模式可以提高代码的可维护性和可扩展性,但是Android程序容易有性能瓶颈,因此要控制设计的度,设计不能太牵强,否则就是过度设计了。常见的设计模式有很多,比如单例模式、工厂模式以及观察者模式等,由于本书不是专门介绍设计模式的书,因此这里就不对设计模式进行详细的介绍了,读者可以参看《大话设计模式》和《Android源码设计模式解析与实战》这两本书,另外设计模式需要理解后灵活运用才能发挥更好的效果。

到这里已经对全书有了详细阅读和记录了。确实写的不错。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章