Android开发

开发十年老架构师:Android性能优化实践,程序员如何应对中年危机

本文主要是介绍开发十年老架构师:Android性能优化实践,程序员如何应对中年危机,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

public static Context context;

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

上述代码在MainActivity中context为静态变量,并持有Context,当Activity退出后,由于Activity被context一直引用着,导致Activity无法被回收,因此造成了内存泄漏。上述代码比较明显,一般不会犯这种错误。

例子2:

public class MainActivity extends Activity {
public static Out mOut;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mOut = new Out(this);
}
}
//外部Out类
public class Out {
Out(Context context) {

}
}

上述代码与例子1类似,mOut为静态变量,生命周期与应用一致,传入的MainActivity也被一直引用,导致Activity无法被回收,造成内存泄漏。

解决方案:

1、在不使用静态变量时,置空。

2、可以使用Application的Context。

3、通过弱引用和软引用来引用Activity。

  • 强引用:直接的对象引用。
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被GC回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象随时被GC回收。

例子3:

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。

public class Singleton {
private static Singleton singleton = null;
private Context mContext;

public Singleton(Context mContext) {
this.mContext = mContext;
}

public static Singleton getSingleton(Context context){
if (null == singleton){
singleton = new Singleton(context);
}
return singleton;
}
}

像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。

当我们退出Activity时,该Activity就没有用了,但是因为singleton作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。

2、非静态内部类导致内存泄露

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。

非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:

public class MainActivity extends AppCompatActivity {

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

private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}

private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 做相应逻辑
}
}
};
}

当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。
通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。

public class MainActivity extends AppCompatActivity {

private Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}

private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}

private static class MyHandler extends Handler {

private WeakReference activityWeakReference;

public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 做相应逻辑
}
}
}
}
}

mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。
上面的做法确实避免了Activity导致的内存泄露,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}

非静态内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask。
比如在Activity中直接new一个子线程Thread:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
// 模拟相应耗时逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

或者直接新建AsyncTask异步任务:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void… params) {
// 模拟相应耗时逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
}

这种方式新建的子线程Thread和AsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。要避免内存泄露的话还是需要像上面Handler一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder的正确写法)。

3、集合类内存泄露

集合类添加元素后,将会持有元素对象的引用,导致该元素对象不能被垃圾回收,从而发生内存泄漏。

4、未关闭资源对象内存泄露

  • 注销广播:如果广播在Activity销毁后不取消注册,那么这个广播会一直存在系统中,由于广播持有了Activity的引用,因此会导致内存泄露。
  • 关闭输入输出流等:在使用IO、File流等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果不及时关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就应该及时关闭,以便缓冲能得到释放,从而避免内存泄露。
  • 回收Bitmap:Bitmap对象比较占内存,当它不再被使用的时候,最好调用Bitmap.recycle()方法主动进行回收。
  • 停止动画:属性动画中有一类无限动画,如果Activity退出时不停止动画的话,动画会一直执行下去。因为动画会持有View的引用,View又持有Activity,最终Activity就不能给回收掉。只要我们在Activity退出把动画停掉即可。
  • 销毁WebView:WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。或是把使用了 WebView 的 Activity (或者 Service) 放在单独的进程里

WebView扩展:

WebView 解析网页时会申请Native堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。
由于占用的都是Native 堆内存,所以实际占用的内存大小不会显示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虚拟机分配的内存,即使Native堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆内存信息。

5、使用系统服务引发的内存泄漏

为了方便我们使用一些常见的系统服务,Activity 做了一些封装。比如说,可以通过 getPackageManager在 Activtiy 中获取 PackageManagerService,但是,里面实际上调用了 Activity 对应的 ContextImpl 中的 getPackageManager 方法

ContextWrapper#getPackageManager

@Override
public PackageManager getPackageManager() {
return mBase.getPackageManager();
}
ContextImpl#getPackageManager

@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn’t matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));//创建 ApplicationPackageManager
}
return null;
}

ApplicationPackageManager#ApplicationPackageManager

ApplicationPackageManager(ContextImpl context,
IPackageManager pm) {
mContext = context;//保存 ContextImpl 的强引用
mPM = pm;
}

private UserManagerService(Context context, PackageManagerService pm,
Object packagesLock, File dataDir) {
mContext = context;//持有外部 Context 引用
mPm = pm;
//代码省略
}
PackageManagerService#PackageManagerService

public class PackageManagerService extends IPackageManager.Stub {
static UserManagerService sUserManager;//持有 UMS 静态引用
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
}
}

遇到的内存泄漏问题是因为在 Activity 中调用了 getPackageManger 方法获取 PMS ,该方法调用的是 ContextImpl,此时如果ContextImpl 中 PackageManager 为 null,就会创建一个 PackageManger(ContextImpl 会将自己传递进去,而 ContextImpl 的 mOuterContext 为 Activity),创建 PackageManager 实际上会创建 PackageManagerService(简称 PMS),而 PMS 的构造方法中会创建一个 UserManger(UserManger 初始化之后会持有 ContextImpl 的强引用)。
只要 PMS 的 class 未被销毁,那么就会一直引用着 UserManger ,进而导致其关联到的资源无法正常释放。

解决办法:

将getPackageManager()改为 getApplication()#getPackageManager() 。这样引用的就是 Application Context,而非 Activity 了。

内存泄漏工具

1、leakcanary

leakcanary是square开源的一个库,能够自动检测发现内存泄露,其使用也很简单:
在build.gradle中添加依赖:

dependencies {
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.1’
releaseIm
plementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.1’

//可选项,如果使用了support包中的fragments
debugImplementation ‘com.squareup.leakcanary:leakcanary-support-fragment:1.6.1’
}

根目录下的build.gradle添加mavenCentral()即可,如下:

allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

然后在自定义的Application中调用以下代码就可以了。

public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);

//正常初始化代码
}
}

如果检测到有内存泄漏,通知栏会有提示,如下图;如果没有内存泄漏,则没有提示。

2、Memory Profiler

Memory Profiler 是 Android Profiler 中的一个组件,可以帮助你分析应用卡顿,崩溃和内存泄露等等问题。

打开 Memory Profiler后即可看到一个类似下图的视图。

上面的红色数字含义如下:

1.用于强制执行垃圾回收事件的按钮。
2.用于捕获堆转储的按钮。
3.用于记录内存分配情况的按钮。 此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示。
4.用于放大/缩小/还原时间线的按钮。
5.用于跳转至实时内存数据的按钮。
6.Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event。
7.内存使用量时间线,其包含以下内容:
一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示。
虚线表示分配的对象数,如右侧的 y 轴所示。
用于表示每个垃圾回收事件的图标。

如何Memory Profiler分析内存泄露,按以下步骤来即可:

1.使用Memory Profiler监听要分析的应用进程
2.旋转几次要分析的Activity。(这是因为旋转Activity后会重新创建)
3.点击捕获堆转储按钮去捕获堆转储
4.在捕获结果中搜索要分析的类。(这里是MainActivity)
5.点击要分析的类,右边会显示这个类创建对象的数量。

如下图:

在这里插入图片描述

内存抖动

内存抖动的原因:

内存抖动一般是瞬间创建了大量对象,会在短时间内触发多次GC,产生卡顿。

内存抖动的在分析工具上的表现:
在这里插入图片描述

解决方案:

最简单的做法就是把之前的主线程操作放到子线程去,虽然内存抖动依然存在,但是卡顿问题可以大大缓解。

对于内存抖动本身:

尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
需要大量使用Bitmap和其他大型对象时,尽量尝试复用之前创建的对象。

网络优化

客户端请求流程如下:

在这里插入图片描述

相关分析工具

分析网络情况的方式可以通过Wireshark, Fiddler, Charlesr等抓包工具,也可以通过Android Studio的Network Profiler

窗口顶部显示的是 Event 时间线以及 1 无线装置功耗状态(低/高)与 WLAN 的对比。 在时间线上,您可以 2点击并拖动选择时间线的一部分来检查网络流量。

下方的3窗口会显示在时间线的选定片段内收发的文件,包括文件名称、大小、类型、状态和时间。 您可以点击任意列标题为此列表排序。

同时,您还可以查看时间线选定片段的明细数据,显示每个文件的发送或接收时间。

点击网络连接的名称即可查看 4 有关所发送或接收的选定文件的详细信息。 点击各个标签可查看响应数据、标题信息或调用堆栈。

在这里插入图片描述

注: 必须启用高级分析才能从时间线中选择要检查的片段,查看发送和接收的文件列表,或查看有关所发送或接收的选定文件的详细信息。 要启用高级分析,请参阅启用高级分析。

启用高级分析需要点击Run Configuration:

在这里插入图片描述

打开Run/Debug Configurations,左侧选择你的应用,右侧在Profiling中勾选Enable advanced profiling。

在这里插入图片描述

通过以上这些工具可以查看某个时间段内网络请求的具体情况,从而进行网络优化的相关工作。

优化建议

1、后端API设计

后端设计API时需要考虑网络请求的频次、资源状态,在某些情况下可以合并多个接口以满足客户端业务需求。

2、Gzip压缩

使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗。同时可以考虑使用Protocol Buffer代替JSON,protobuf会比JSON数据量小很多.

3、图片大小优化

  • 请求图片时告诉服务器需要的图片的宽高。(比如使用七牛时,可以在url后面添加质量、格式、宽高等等来获取合适的图片资源)
  • 列表采用缩略图。
  • 使用Webp格式:安卓系统从Android4.0(API 14)添加了有损耗的WebP support并且在Android4.2(API 17)对无损的,清晰的WebP提供了支持。使用WebP格式;同样的照片,采用WebP格式可大幅节省流量,相对于JPG格式的图片,流量能节省将近 25% 到 35 %;相对于PNG格式的图片,流量可以节省将近80%。最重要的是使用WebP之后图片质量也没有改变。
  • 使用第三方图片加载框架
  • 网络缓存
  • 监听网络状态,非WiFi下可以显示无图页面,WiFi或4G情况下才显示有图页面。
  • IP直连与HttpDns:DNS解析的失败率占联网失败中很大一种,而且首次域名解析一般需要几百毫秒。针对此,我们可以不用域名,才用IP直连省去 DNS 解析过程,节省这部分时间。HttpDNS基于Http协议的域名解析,替代了基于DNS协议向运营商Local DNS发起解析请求的传统方式,可以避免Local DNS造成的域名劫持和跨网访问问题,解决域名解析异常带来的困扰。

电量优化

电量分析工具

1、Batterystats & bugreport

Android 5.0及以上的设备, 允许我们通过adb命令dump出电量使用统计信息.

因为电量统计数据是持续的, 会非常大, 统计我们的待测试App之前先reset下, 连上设备,命令行执行:

$ adb shell dumpsys batterystats --reset
Battery stats reset.

断开测试设备, 操作我们的待测试App,重新连接设备, 使用adb命令导出相关统计数据:

// 此命令持续记录输出, 想要停止记录时按Ctrl+C退出.
$ adb bugreport > bugreport.txt

导出的统计数据存储到bugreport.txt, 此时我们可以借助如下工具来图形化展示电池的消耗情况。

2、Battery Historian

Google提供了一个开源的电池历史数据分析工具

Battery Historian链接

耗电原因
  • 网络请求
  • 使用WakeLock:WakeLock会保持CPU运行,或是防止屏幕变暗/关闭,让手机可以在用户不操作时依然运行。CPU会一直得不到休眠, 而大大增加耗电.
  • GPS
  • 蓝牙传输

建议:
根据具体业务需求,严格限制应用位于后台时是否禁用某些数据传输,尽量能够避免无效的数据传输。
数据传输的频度问题,如网络请求可以压缩合并,如本地数据上传,可以选择恰当的时机上传。

JobScheduler组件

通过不停的唤醒CPU(通过后天常驻的Service)来达到一些功能的使用,这样会造成电量资源的消耗,比如后台日志的上报,定期更新数据等等,在Android 5.0提供了一个JobScheduler组件,通过设置一系列的预置条件,当条件满足时,才执行对应的操作,这样既能省电,有保证了功能的完整性。

JobScheduler的适用场景:

  • 重要不紧急的任务,可以延迟执行,比如定期数据库数据更新和数据上报
  • 耗电量较大的任务,比如充电时才执行的备份数据操作。
  • 不紧急可以不执行的网络任务,比如在Wi-Fi环境下预加载数据。
  • 可以批量执行的任务
  • …等等

JobScheduler的使用

private Context mContext;
private JobScheduler mJobScheduler;

public JobSchedulerManager(Context context){
this.mContext=context;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
this.mJobScheduler= (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
}
}

通过getSystemService()方法获取一个JobSchedule的对象。

public boolean addTask(int taskId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
JobInfo.Builder builder = new JobInfo.Builder(taskId,
new ComponentName(“com.apk.administrator.loadapk”,
JobScheduleService.class.getName()));
switch (taskId) {
case 1:
//每隔1秒执行一次
builder.setPeriodic(1000);
break;
case 2:
//设备重启后,不再执行该任务
builder.setPersisted(false);
break;
default:
break;
}
if (null != mJobScheduler) {
return mJobScheduler.schedule(builder.build()) > 0;
} else {
return false;
}
} else {
return true;
}
}

创建一个JobInfo对象时传入两个参数,第一个参数是任务ID,可以对不同的任务ID做不同的触发条件,执行任务时根据任务ID执行具体的任务;第二个参数是JobScheduler任务的服务,参数为进程名和服务类名。

JobInfo支持以下几种触发条件:

  • setMinimumLatency(long minLatencyMillis):设置任务的延迟时间(单位是ms),需要注意的是,setMinimumLatency与setPeriodic(long time)方法不兼容,同时调用会引起异常。
  • setOverrideDeadline(long maxExecutionDelayMillis):设置任务最晚的延迟时间。如果到了规定时间,其它条件还未满足,这个任务也会被启动。与setMinimumLatency(long time)一样,setOverriddeDeadline与setPeriodic(long time)同时调用会引起异常。
  • setPersisted(boolean isPersisted):设置重启之后,任务是否还要继续执行。
  • setRequiredNetworkType(int networkType):只有满足指定的网络条件时,才会被执行。有三种网络条件,JobInfo.NETWORK_TYPE_NONE不管是否有网络,这个任务都会被执行(如果未设置,这个参数就是默认参数);JobInfo.NETWORK_TYPE_ANY只有在有网络的情况下,任务才会执行,和网络类型无关;JobInfo.NETWORK_TYPE_UNMETERED非运营商网络(比如在Wi-Fi连接时),任务才会被执行。
  • setRequiresCharging(boolean * requiresCharging):只有当设备在充电时,这个任务才会被执行。
    setRequiresDeviceIdle(boolean * requiresDeviceIdle):只有当用户没有在使用该设备且有一段时间没有使用时,才会启动该任务。

public class JobScheduleService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
return false;
}

@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}

JobService运行在主线程,如果是耗时任务,使用ThreadHandler或者一个异步任务来运行耗时的任务,防止阻塞主线程。
务都会被执行(如果未设置,这个参数就是默认参数);JobInfo.NETWORK_TYPE_ANY只有在有网络的情况下,任务才会执行,和网络类型无关;JobInfo.NETWORK_TYPE_UNMETERED非运营商网络(比如在Wi-Fi连接时),任务才会被执行。

  • setRequiresCharging(boolean * requiresCharging):只有当设备在充电时,这个任务才会被执行。
    setRequiresDeviceIdle(boolean * requiresDeviceIdle):只有当用户没有在使用该设备且有一段时间没有使用时,才会启动该任务。

public class JobScheduleService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
return false;
}

@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}

JobService运行在主线程,如果是耗时任务,使用ThreadHandler或者一个异步任务来运行耗时的任务,防止阻塞主线程。

这篇关于开发十年老架构师:Android性能优化实践,程序员如何应对中年危机的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!