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。
例子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、未关闭资源对象内存泄露
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、图片大小优化
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链接
建议:
根据具体业务需求,严格限制应用位于后台时是否禁用某些数据传输,尽量能够避免无效的数据传输。
数据传输的频度问题,如网络请求可以压缩合并,如本地数据上传,可以选择恰当的时机上传。
通过不停的唤醒CPU(通过后天常驻的Service)来达到一些功能的使用,这样会造成电量资源的消耗,比如后台日志的上报,定期更新数据等等,在Android 5.0提供了一个JobScheduler组件,通过设置一系列的预置条件,当条件满足时,才执行对应的操作,这样既能省电,有保证了功能的完整性。
JobScheduler的适用场景:
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支持以下几种触发条件:
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连接时),任务才会被执行。
public class JobScheduleService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
JobService运行在主线程,如果是耗时任务,使用ThreadHandler或者一个异步任务来运行耗时的任务,防止阻塞主线程。