开发十年老架构师:Android性能优化实践,2024年最新腾讯的面试题

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’
releaseImplementation ‘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或者一个异步任务来运行耗时的任务,防止阻塞主线程。

JobScheduleService继承JobService,实现两个方法onStartJob和onStopJob。
任务开始时,执行onStartJob方法,当任务执行完毕后,需要调用jobFinished方法来通知系统;任务执行完成后,调用jobFinished方法通知JobScheduler;当系统接受到一个取消请求时,调用onStopJob方法取消正在等待执行的任务。如果系统在接受到一个取消请求时,实际任务队列中已经没有正在运行的任务,onStopJob不会被调用。

最后在AndroidManifest中配置下:

APK体积优化

1、从图片入手:.9图、压缩或采用Webp。
2、使用Lint删除无用资源
3、通过Gradle配置,过滤无用资源和.so文件
4、第三方库慎重使用,可以只提取使用到的代码
5、资源混淆:方案有:美团和微信,前者是通过修改AAPT在处理资源文件相关的源码达到资源名的替换,后者通过直接修改resources.arsc文件来达到资源文件名的混淆。
6、插件化

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

学习分享,共勉

Android高级架构师进阶之路

题外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人

  • Android进阶知识体系学习脑图

  • Android进阶高级工程师学习全套手册

  • 对标Android阿里P7,年薪50w+学习视频

  • 大厂内部Android高频面试题,以及面试经历

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人

  • Android进阶知识体系学习脑图

[外链图片转存中…(img-8rNZ4qdE-1712729967365)]

  • Android进阶高级工程师学习全套手册

[外链图片转存中…(img-yLC3srzu-1712729967365)]

  • 对标Android阿里P7,年薪50w+学习视频

[外链图片转存中…(img-hMcf03zT-1712729967365)]

  • 大厂内部Android高频面试题,以及面试经历

[外链图片转存中…(img-0WTOrMc0-1712729967365)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-5es39kPV-1712729967366)]

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值