安卓代码优化

 我们的目标是写出更加高效的代码。根据Android官方的建议,编写高效代码的两个基本准则如下:
.不要做冗余的工作。
.尽量避免次数过多的内存分配操作。
其实还要加上第三个准则:深入的理解所用语言特性和系统平台的API,具体到Android开发,就是要熟练掌握Java语言,并对Android SDK 所提供的API了如指掌。
1.数据结构的选择
2.Handler和内部类的正确用法
3.正确的使用Context
4.掌握Java的四种引用方式
5.其他代码微优化

一.数据结构的选择
正确选择合适的数据结构是很重要的,对Java中常见的数据结构例如ArrayList和LinkedList,HashMap 和 HashSet等,需要做到对它们的联系与区别有深入的理解,这样在编写代码中面临选择时才能做出正确的选择。

以Android开发中SparseArray代替HashMap为例:


SparseArray是Android平台特有的稀疏数组的实现,它是Integer到Object的一个映射,在特定场合可用于代替HashMap<Integer,<E>>,提高性能。它的核心实现是二分查找算法, 所以,它存储的数值都是按键值从小到大的顺序排列好的。

SparseArray家族目前有四类:
1.SparseBooleanArray booleanArray = new SparseBooleanArray();
   用于代替HashMap<Integer,Boolean> booleanMap = new HashMap<Integer,Boolean>();
2.SparseIntArray intArray = new SparseIntArray();
   用于代替HashMap<Integer,Integer> booleanMap = new HashMap<Integer,Integer>();
3.SparseLongArray longArray = new SparseLongArray();
   用于代替HashMap<Integer,Long> booleanMap = new HashMap<Integer,Long>();
4.SparseArray<String> stringArray = new SparseArray<String>();
  用于代替HashMap<Integer,String> booleanMap = new HashMap<Integer,String>();

       但是SparseArray不是线程安全的,所以使用时需要注意各种数据结构的特点。而且,由于要进行二分查找,因此SparseArray会对插入的数据按照Key值大小顺序插入。还有一点,SparseArray对删除操作做了优化,它并不会立即删除这个元素,而是通过设置标识位(DELETE)的方式, 这个被标记的元素要么被重复利用,要 么在多次remove之后通过一次gc操作中被挤压出去
       在Android工程中运行Lint进行静态代码分析,会有一个名为AndroidLintUseSparseArrays的检查项,如果违规,它会进行提示如下:
      HashMap can be replaced with SparseArray
这样可以很轻松的找到工程汇总可优化的地方。

Lint 工作方式简单介绍

Lint 会根据预先配置的检测标准检查我们 Android 项目的源文件,发现潜在的 bug 或者可以优化的地方,优化的内容主要包括以下几方面:

Correctness:不够完美的编码,比如硬编码、使用过时 API 等
Performance:对性能有影响的编码,比如:静态引用,循环引用等
Internationalization:国际化,直接使用汉字,没有使用资源引用等
Security:不安全的编码,比如在 WebView 中允许使用 JavaScriptInterface 等
Lint 检测代码的过程如下图: 

App 源文件:包括 Java 代码,XML 代码,图标,以及 ProGuard 配置文件等
lint.xml:Lint 检测的执行标准配置文件,我们可以修改它来允许或者禁止报告一些问题

命令行运行 Lint

 window: gradlew lint     mac: ./gradlew lint

Lint 的使用路径: 
工具栏 -> Analyze -> Inspect Code…

Lint 的警告严重程度有以下几种:
Unused Entry:没有使用的属性,灰色,很不起眼
Typo:拼写错误,绿色波浪下划线,也不太起眼
Server Problem:服务器错误?好像不是
Info:注释文档,绿色,比较显眼
Weak Warning:比较弱的警告,提示比较弱
Warning:警告,略微显眼一点
Error:错误,最显眼的一个

代码迭代版本一多,很容易会遗留一些无用的代码、资源文件,我们可以使用 Lint 进行清除:
      点击 Android Studio 工具栏 -> Analyze -> Run Inspection By Name..,输入要检测的内容
  
二.Handler和内部类的正确用法
Android代码中涉及线程间通信的地方经常会使用Handler,典型的代码结构如下所示。

使用Android Lint 分析这段代码,会违反检查项,有如下提示
This Handler class should be static or leaks might occur
       Handler被声明成了一个内部类,这可能会阻止垃圾回收机制对它所持有的外部类的回收。但是,如果这个Handler所使用的不是主线程的Looper或者MessageQueue的话就不会有这个问题了。注意:如果是主线程的话,就有可能发生内存泄露的情况。

       那么产生内存泄漏的原因可能是什么呢?我们知道Handler是和Looper以及MessageQueue一起工作的,在Android中,一个应用启动后,系统会默认创建一个为主线程服务的Looper对象,该Looper对象用于处理主线程的所有Message对象,它的生命周期贯穿于整个应用的生命周期。在主线程中使用的Handler都会默认绑定到这个对象。在主线程中创建Handler对象时,它会立即关联主线程Looper对象的MessageQueue,这时发送到MessageQueue中的Message对象都会持有这个Handler对象的引用,这样在Looper处理消息时才能回调到Handler的HandlerMessage方法。因此,如果Message还没有被处理完成,那么Handler对象也就不会被垃圾回收。

        在上面的代码中,将Handler的实例声明为HandlerActivity类的内部类。而在Java语言中非静态内部匿名类会持有外部类的一个隐式的引用,这样就可能会导致外部类无法被垃圾回收。因此,最终由于MessageQueue中的Message还没处理完成,就会持有Handler对象的引用,而非静态的Handler对象会持有外部类HandlerActivity引用,这个Activity无法被垃圾回收,从而导致内存泄露。
例如:

由于消息延迟5分钟发送,因此,当用户进入这个Activity并退出后,在消息发送并处理完成之前,这个Activity是不会被系统回收的(当然,系统内存确实不够使用的情况例外。)
那么这个问题如何解决呢?
第一个方案:在子线程中使用Handler,这时需要开发者自己创建一个Looper对象,这个Looper对象的生命周期同一般的Java对象,因此这种用法没有问题。
第二个方案:将Handler声明为静态的内部类,因为静态内部类不会持有外部类的引用,因此,也不会引起内存泄露,比如:


扫描二维码关注公众号,回复: 2422950 查看本文章
三.正确使用Context
Context应该是入门开发接触到的第一个概念,它代表当前的上下文环境,可以用来实现很多功能的调用。
//获取资源管理器对象,进而可以访问到例如string,color等资源
Resources resources = context.getResources();

//启动指定的Activity
context.startActivity(new Intent(this,MainActivity.class));

//获取各种系统服务
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);

//获取系统文件目录
File internalDir = context.getCacheDir();
File externalDir = context.getExternalCacheDir();
应用开发中随处可见Context的使用,但是不是所有的Context实例都具备相同的功能,在使用上需要区别对待,否则很可能会引入问题。
1,Context的种类

根据Context依托的组件以及用途不同,可以将Context分为以下几种
1)Application:Android 应用中的默认单例类,在Activity或者Service中通过getApplication()可以获取到这个单例,通过context.getApplicationContext()可以获取到应用全局唯一的Context实例。
2)Activity/Service: 这两个类都是ContextWrapper的子类,在这两个类中看通过getBaseContext()获取到它们的Context实例,不同的Activity或者service实例,它们的Context都是独立的,不会复用。
3)BroadcastReceiver:和Activity以及Service不同,BroadcastReceiver本身并不是Context的子类,而是在回调函数onReceive()中由Android框架传入一个Context的实例。系统传入的这个Context实例是经过功能裁剪的,它不能调用registerReceiver()以及bindService()这两个函数。
4)ContentProvider:同样的,ContentProvider也不是Context 的子类,但在创建时,系统会传入一个Context实例,这样在ContentProvider中可以通过调用getContext()函数获取。如果ContentProvider和调用者处于相同的应用进程中,那么getContext()将返回应用全局唯一的Context实例。如果是其他进程调用的ContentProvider,那么ContentProvider将持有自身所在进程的Context实例。

2.错误使用Context导致的内存泄露

使用的时候传入的是activity或者service 在应用退出前,由于单例一直在,会到只对应的activity或者service 一直被引用而不能被垃圾回收,activity或者service关联的其他View也不会被释放而导致内存泄露  
3.不同Context对比
不同组件中的Context能提供的功能不尽相同

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:
  • 数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

  • 数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

  • 数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。但这两个都不会被统计到App context个数中。
注意Context引用的持有,防止内存泄漏。关于这一点,有如下三个建议:

1:不要长时间持有 组件的Context,(持有的情况可能有 workThread, static 变量,non-static inner Class)

2:对于不受控的非静态内部类,建议修改成静态内部类,同时采用弱引用的方式 引用 Activity/Service 的Context。

3:其他可以使用Application Context 的地方,就用Application Context。

四.掌握Java的四种引用方式
1.四种引用
1.1、强引用
当我们使用new 这个关键字创建对象时被创建的对象就是强引用,如Object object = new Object() 这个Object()就是一个强引用了,如果一个对象具有强引用。垃圾回收器就不会去回收有强引用的对象。如当jvm内存不足时,具备强引用的对象, 虚拟机宁可会报内存空间不足的异常来终止程序,也不会靠垃圾回收器去回收该对象来解决内存。
1.2、软引用
如果一个对象具备软引用,如果内存空间足够,那么垃圾回收器就不会回收它,如果内存空间不足了,就会回收该对象。当然没有被回收之前,该对象依然可以被程序调用。
1.3、弱引用
如果一个对象只具有弱引用,只要垃圾回收器在自己的内存空间中线程检测到了,就会立即被回收,对应内存也会被释放掉。相比软引用弱引用的生命周期要比软引用短很多。不过,如果垃圾回收器是一个优先级很低的线程,也不一定会很快就会释放掉软引用的内存。
1.4、虚引用
如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被jvm当作垃圾进行回收

2、引用和队列的使用
强引用一般是不会和队列一起使用的,这个过滤掉。
软引用可以和一个引用队列来联合使用,一般软引用可以用来实现内存敏感的高速缓存,如果软引用的所引用的对象被垃圾回收,java虚拟机就会把引用入到与之关联的引用队列中去。
弱引用和队列使用在一起,如果弱引用被所引用的对象回收了,java虚拟机就会把这个弱引用加入到关联的队列中去;

虚引用,在java虚拟机回收虚引用时,会把这个虚引用放到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经引用了虚引用,来了解引用对象是否要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么可以在所引用的对象内存前,采取一些逻辑处理。


五.其他代码微优化
1,避免创建非必要的对象
Android设备不像PC那样有着足够大的内存,而且单个App占用的内存实际上是比较小的。所以避免创建不必要的对象对于Android开发尤为重要。
对象的创建需要内存分配,对象的销毁需要垃圾回收,这些都会一定程度上影响应用的性能,因此一般来说,最好是重用对象,而不是在每次需要的是互去创建一个功能相同的新对象,特别是注意不要在循环中重复创建相同的对象。

2.对常量使用static final 修饰
我们先来看一下在一个类的最顶部定义如下代码:
  1. static int intVal = 42;  
  2. static String strVal = "Hello, world!"
经过这样修改之后,定义类就不再需要一个<clinit>方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal时会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。
另外需要大家注意的是,这种优化方式只对基本数据类型以及String类型的常量有效,对于其它数据类型的常量是无效的。不过,对于任何常量都是用static final的关键字来进行声明仍然是一种非常好的习惯。

3. 避免在内部调用Getters/Setters方法
我们平时写代码时都被告知,一定要使用面向对象的思维去写代码,而面向对象的三大特性我们都知道,封装、多态和继承。其中封装的基本思想就是不要把类内部的字段暴漏给外部,而是提供特定的方法来允许外部操作相应类的内部字段,从而在Java语言当中就出现了Getters/Setters这种封装技巧。

然而在Android上这个技巧就不再是那么的受推崇了,因为字段搜寻要比方法调用效率高得多,我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍。不过我们肯定不能仅仅因为效率的原因就将封装这个技巧给抛弃了,编写代码还是要按照面向对象思维的,但是我们可以在能优化的地方进行优化,比如说避免在内部调用getters/setters方法。

那什么叫做在内部调用getters/setters方法呢?例如:

这是一个Calculate类,这个类的功能非常简单,先将one和two这两个字段进行了封装,然后提供了getOne()方法获取one字段的值,提供了getTwo()方法获取two字段的值,还提供了一个getSum()方法用于获取总和的值。

这里我们注意到,getSum()方法当中的算法就是将one和two的值相加进行返回,但是它获取one和two的值的方式也是通过getters方法进行获取的,其实这是一种完全没有必要的方式,因为getSum()方法本身就是Calculate类内部的方法,它是可以直接访问到Calculate类中的封装字段的,因此这种写法在Android上是不推崇的,我们可以进行如下修改:

改成这种写法之后,我们就避免了在内部调用getters/setters方法,而对于外部而言Calculate类仍然是具有很好的封装性的。

4. 静态优于抽象
如果你并不需要访问一个对象中的某些字段,只是想调用它的某个方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,这会让调用的速度提升15%-20%,同时也不用为了调用这个方法而去专门创建对象了,这样还满足了上面的一条原则。另外这也是一种好的编程习惯,因为我们可以放心地调用静态方法,而不用担心调用这个方法后是否会改变对象的状态(静态方法内无法访问非静态字段)

5. 多使用系统封装好的API
我们在编写程序时如果可以使用系统提供的API就应该尽量使用,系统提供的API完成不了我们需要的功能时才应该自己去写,因为使用系统的API在很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。

6.代码的重构
不同的程序员有不同的编写代码的习惯,而代码的重构是一项长期的持之以恒的工作。

猜你喜欢

转载自blog.csdn.net/qq_36282231/article/details/81035408