看到这些头皮发麻吧,而且是一个大隔断间里面所有的小隔断,小小隔断等等都测量完摆放好,才能换另外一个大隔断,天呢,太浪费时间了,不能都直接都放外面吗?也好摆放啊,这么搞我怎么摆,每个隔断间都要装修一遍,太浪费时间了啊。

我们的Android虚拟机也会这么抱怨,咱们家本来就不富裕,什么都要省着用,你这么搞,肯定运转有问题啊,那么多嵌套的小隔断间需要处理,都会占用cpu计算的时间和GPU渲染的时间。显示GPU过度绘制,分层如下如所示:

通过颜色我们可以知道我们应用是否有多余层次的绘制,如果一路飘红,那么我们就要相应的处理了。

所以我们有了第一个优化版本:

优化 1.0

如果父控件有颜色,也是自己需要的颜色,那么就不必在子控件加背景颜色如果每个自控件的颜色不太一样,而且可以完全覆盖父控件,那么就不需要再父控件上加背景颜色尽量减少不必要的嵌套能用LinearLayout和FrameLayout,就不要用RelativeLayout,因为RelativeLayout控件相对比较复杂,测绘也想要耗时。

做到了以上4点只能说恭喜你,入门级优化已经实现了。

针对嵌套布局,谷歌也是陆续出了一些新的方案。对就是include、merge和ViewStub三兄弟。

include可以提高布局的复用性,大大方便我们的开发,有人说这个没有减少布局的嵌套吧,对,include确实没有,但是include和merge联手搭配,效果那是杠杠滴。

merge的布局取决于父控件是哪个布局,使用merge相当于减少了自身的一层布局,直接采用父include的布局,当然直接在父布局里面使用意义不大,所以会和include配合使用,既增加了布局的复用性,用减少了一层布局嵌套。

ViewStub它可以按需加载,什么意思?用到他的时候喊他一下,再来加载,不需要的时候像空气一样,在一边静静的呆着,不吃你的米,也不花你家的钱。等需要的时候ViewStub中的布局才加载到内存,多节俭持家啊。对于一些进度条,提示信息等等八百年才用一次的功能,使用ViewStub是极其合适的。这就是不用不知道,一用戒不了。

我们开始进化我们的优化

优化 1.1

使用include和merge增加复用,减少层级ViewStub按需加载,更加轻便

可能又有人说了:背景复用了,嵌套已经很精简了,再精简就实现了不了复杂视图了,可是还是一路飘红,这个怎么办?面对这个问题谷歌给了我们一个新的布局ConstraintLayout。

ConstraintLayout可以有效地解决布局嵌套过多的问题。ConstraintLayout使用约束的方式来指定各个控件的位置和关系的,它有点类似于 RelativeLayout,但远比RelativeLayout要更强大(照抄隔壁IOS的约束布局)。所以简单布局简单处理,复杂布局ConstraintLayout很好使,提升性能从布局做起。

再次进化:

优化 1.2

复杂界面可选择ConstraintLayout,可有效减少层级

2、绘制优化

我们把布局优化了,但是和布局息息相关的还有绘制,这是直接影响显示的两个根本因素。

其实布局优化了对于性能提升影响不算很大,但是是我们最容易下手,最直接接触的优化,所以不管能提升多少,哪怕只有百分之一的提升,我们也要做,因为影响性能的地方太多了,每个部分都提升一点,我们应用就可以提升很多了。

我们平时感觉的卡顿问题最主要的原因之一是因为渲染性能,因为越来越复杂的界面交互,其中可能添加了动画,或者图片等等。我们希望创造出越来越炫的交互界面,同时也希望他可以流畅显示,但是往往卡顿就发生在这里。

这个是Android的渲染机制造成的,Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,但是渲染未必成功,如果成功了那么代表一切顺利,但是失败了可能就要延误时间,或者直接跳过去,给人视觉上的表现,就是要么卡了一会,要么跳帧。

View的绘制频率保证60fps是最佳的,这就要求每帧绘制时间不超过16ms(16ms = 1000/60),虽然程序很难保证16ms这个时间,但是尽量降低onDraw方法中的复杂度总是切实有效的。

这个正常情况下,每隔16ms draw()一下,很整齐,很流畅,很完美。

往往会发生如下图的情况,有个便秘的家伙霸占着,一帧画面拉的时间那么长,这一下可不就卡顿了嘛。把后面的时间给占用了,后面只能延后,或者直接略过了。

既然问题找到了,那么我们肯定要有相应的解决办法,根本做法是 减轻onDraw()的负担。

所以

第一点: onDraw方法中不要做耗时的任务,也不做过多的循环操作,特别是嵌套循环,虽然每次循环耗时很小,但是大量的循环势必霸占CPU的时间片,从而造成View的绘制过程不流畅。

第二点: 除了循环之外,onDraw()中不要创建新的局部对象,因为onDraw()方法一般都会频繁大量调用,就意味着会产生大量的零时对象,不进占用过的内存,而且会导致系统更加频繁的GC,大大降低程序的执行速度和效率。

其实这两点在android的UI线程中都适用。

升级进化:

优化2.0

onDraw中不要创建新的局部对象onDraw方法中不要做耗时的任务

其实从渲染优化里我们也牵扯出了另一个优化,那就是内存优化。

3、内存优化

内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了没来就不多的内存空间。

内存泄漏是一个缓慢积累的过程,一点一点的给你,温水煮青蛙一般,我们往往很难直观的看到,只能最后内存不够用了,程序奔溃了,才知道里面有大量的泄漏,但是到底是那些地方?估计是狼烟遍地,千疮百孔,都不知道如何下手。怎么办?最让人难受的是内存泄漏情况那么多,记不住,理解也不容易,关键是老会忘记。怎么办呢?老这么下去也不是事,总不能面试的时候突击,做项目的时候不知所措吧。所以一定要记住了解GC原理,这样才可以更准确的理解内存泄漏的场景和原因。不懂GC原理的可以先看一下这个JVM初探:内存分配、GC原理与垃圾收集器

本来GC的诞生是为了让java程序员更加轻松(这一点隔壁C++痛苦的一匹),java虚拟机会自动帮助我们回收那些不再需要的内存空间。通过引用计数法,可达性分析法等等方法,确认该对象是否没有引用,是否可以被回收。

有人会说真么强悍的功能看起来无懈可击啊,对,理论上可以达到消除内存泄漏,但是很多人不按常理出牌啊,往往很多时候,有的对象还保持着引用,但逻辑上已经不会再用到。就是这一类对象,游走于GC法律的边缘,我没用了,但是你又不知道我没用了,就是这么赖着不走,空耗内存。

因为有内存泄漏,所以内存被占用越来越多,那么GC会更容易被触发,GC会越来越频发,但是当GC的时候所有的线程都是暂停状态的,需要处理的对象数量越多耗时越长,所以这也会造成卡顿。

那么什么情况下会出现这样的对象呢? 基本可以分为以下四大类: 1、集合类泄漏 2、单例/静态变量造成的内存泄漏 3、匿名内部类/非静态内部类 4、资源未关闭造成的内存泄漏

1、集合类泄漏

集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露。

举个栗子:

static List mList = new ArrayList<>(); for (int i = 0; i < 100; i++) { Object obj = new Object(); mList.add(obj); obj = null; }

当mList没用的时候,我们如果不做处理的话,这就是典型的占着茅坑不拉屎,mList内部持有者众多集合元素的对象,不泄露天理难容啊。解决这个问题也超级简单。把mList清理掉,然后把它的引用也给释放掉。

mList.clear(); mList = null;

2、单例/静态变量造成的内存泄漏

单例模式具有其 静态特性,它的生命周期 等于应用程序的生命周期,正是因为这一点,往往很容易造成内存泄漏。 先来一个小栗子:

public class SingleInstance {

private static SingleInstance mInstance; private Context mContext;

private SingleInstance(Context context){ this.mContext = context; }

public static SingleInstance newInstance(Context context){ if(mInstance == null){ mInstance = new SingleInstance(context); } return sInstance; } }

当我们在Activity里面使用这个的时候,把我们Acitivty的context传进去,那么,这个单例就持有这个Activity的引用,当这个Activity没有用了,需要销毁的时候,因为这个单例还持有Activity的引用,所以无法GC回收,所以就出现了内存泄漏,也就是生命周期长的持有了生命周期短的引用,造成了内存泄漏。

所以我们要做的就是生命周期长的和生命周期长的玩,短的和短的玩。就好比你去商场,本来就是传个话的,话说完就要走了,突然保安过来非要拉着你的手,说要和你天长地久。只要商场在一天,他就要陪你一天。天呢?太可怕了。叔叔我们不约,我有我的小伙伴,我还要上学呢,你赶紧找你的保洁阿姨去吧。你在商场的生命周期本来可能就是1分钟,而保安的生命周期那是要和商场开关门一致的,所以不同生命周期的最好别一起玩的好。

解决方案也很简单:

public class SingleInstance {

private static SingleInstance mInstance; private Context mContext;

private SingleInstance(Context context){ this.mContext = context.getApplicationContext(); }

public static SingleInstance newInstance(Context context){ if(mInstance == null){ mInstance = new SingleInstance(context); } return sInstance; } }

还有一个常用的地方就是Toast。你应该知道和谁玩了吧。

3、匿名内部类/非静态内部类

这里有一张宝图:

非静态内部类他会持有他外部类的引用,从图我们可以看到非静态内部类的生命周期可能比外部类更长,这就是二楼的情况一致了,如果非静态内部类的周明周期长于外部类,在加上自动持有外部类的强引用,我的乖乖,想不泄漏都难啊。

我们再来举个栗子:

public class TestActivity extends Activity {

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); new MyAscnyTask().execute(); }

class MyAscnyTask extends AsyncTask{ @Override protected String doInBackground(Void… params) { try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } return “”; } } }

我们经常会用这个方法去异步加载,然后更新数据。貌似很平常,我们开始学这个的时候就是这么写的,没发现有问题啊,但是你这么想一想,MyAscnyTask是一个非静态内部类,如果他处理数据的时间很长,极端点我们用sleep 100秒,在这期间Activity可能早就关闭了,本来Activity的内存应该被回收的,但是我们知道非静态内部类会持有外部类的引用,所以Activity也需要陪着非静态内部类MyAscnyTask一起天荒地老。好了,内存泄漏就形成了。

怎么办呢?

既然MyAscnyTask的生命周期可能比较长,那就把它变成静态,和Application玩去吧,这样MyAscnyTask就不会再持有外部类的引用了。两者也相互独立了。

public class TestActivity extends Activity {

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); new MyAscnyTask().execute(); } //改了这里 注意一下 static static class MyAscnyTask extends AsyncTask{ @Override protected String doInBackground(Void… params) { try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } return “”; } } }

说完非静态内部类,我再来看看匿名内部类,这个问题很常见,匿名内部类和非静态内部类有一个共同的地方,就是会只有外部类的强引用,所以这哥俩本质是一样的。但是处理方法有些不一样。但是思路绝对一样。换汤不换药。

举个灰常熟悉的栗子:

public class TestActivity extends Activity { private TextView mText; private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); //do something mText.setText(" do someThing"); } };

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); mText = findVIewById(R.id.mText); // 匿名线程持有 Activity 的引用,进行耗时操作 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start();

mHandler. sendEmptyMessageDelayed(0, 100000); }

想必这两个方法是我们经常用的吧,很熟悉,也是这么学的,没感觉不对啊,老师就是这么教的,通过我们上面的分析,还这么想吗?关键是 耗时时间过长,造成内部类的生命周期大于外部类,对弈非静态内部类,我们可以静态化,至于匿名内部类怎么办呢?一样把它变成静态内部类,也就是说尽量不要用匿名内部类。完事了吗?很多人不注意这么一件事,如果我们在handleMessage方法里进行UI的更新,这个Handler静态化了和Activity没啥关系了,但是比如这个mText,怎么说?全写是activity.mText,看到了吧,持有了Activity的引用,也就是说Handler费劲心思变成静态类,自认为不持有Activity的引用了,准确的说是不自动持有Activity的引用了,但是我们要做UI更新的时候势必会持有Activity的引用,静态类持有非静态类的引用,我们发现怎么又开始内存泄漏了呢?处处是坑啊,怎么办呢?我们这里就要引出弱引用的概念了。

引用分为强引用,软引用,弱引用,虚引用,强度一次递减。

强引用 我们平时不做特殊处理的一般都是强引用,如果一个对象具有强引用,GC宁可OOM也绝不会回收它。看出多强硬了吧。

软引用(SoftReference) 如果内存空间足够,GC就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

弱引用(WeakReference) 弱引用要比软引用,更弱一个级别,内存不够要回收他,GC的时候不管内存够不够也要回收他,简直是弱的一匹。不过GC是一个优先级很低的线程,也不是太频繁进行,所以弱引用的生活还过得去,没那么提心吊胆。

虚引用 用的甚少,我没有用过,如果想了解的朋友,可以自行谷歌百度。

所以我们用弱引用来修饰Activity,这样GC的时候,该回收的也就回收了,不会再有内存泄漏了。很完美。

public class TestActivity extends Activity { private TextView mText; private MyHandler myHandler = new MyHandler(TestActivity.this); private MyThread myThread = new MyThread();

private static class MyHandler extends Handler {

WeakReference weakReference;

MyHandler(TestActivity testActivity) { this.weakReference = new WeakReference(testActivity);

}

@Override public void handleMessage(Message msg) { super.handleMessage(msg); weakReference.get().mText.setText(“do someThing”);

} }

private static class MyThread extends Thread {

@Override public void run() { super.run();

try { sleep(100000);

} catch (InterruptedException e) { e.printStackTrace(); } } }

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); mText = findViewById(R.id.mText); myHandler.sendEmptyMessageDelayed(0, 100000); myThread.start(); } //最后清空这些回调 @Override protected void onDestroy() { super.onDestroy(); myHandler.removeCallbacksAndMessages(null); }

4、资源未关闭造成的内存泄漏

网络、文件等流忘记关闭手动注册广播时,退出时忘记 unregisterReceiver()Service 执行完后忘记 stopSelf()EventBus 等观察者模式的框架忘记手动解除注册

这些需要记住又开就有关,具体做法也很简单就不一一赘述了。给大家介绍几个很好用的工具: 1、leakcanary傻瓜式操作,哪里有泄漏自动给你显示出来,很直接很暴力。 2、我们平时也要多使用Memory Monitor进行内存监控,这个分析就有些难度了,可以上网搜一下具体怎么使用。 3、Android Lint 它可以帮助我们发现代码机构 / 质量问题,同时提供一些解决方案,内存泄露的会飘黄,用起来很方便,具体使用方法上网学习,这里不多做说明了。

so

优化3.0

解决各个情况下的内存泄漏,注意平时代码的规范。

4、启动速度优化

不知道大家有没有细心发现,我们的应用启动要比别的大厂的要慢,要花费更多的时间,明明他们的包体更大,app更复杂,怎么启动时间反而比我们的短呢?

但是这块的优化关注的人很少,因为App常常伴有闪屏页,所以这个问题看起来就不是问题了,但是一款好的应用是绝对不允许这样的,我加闪屏页是我的事,启动速度慢绝对不可以。

app启动分为冷启动(Cold start)、热启动(Hot start)和温启动(Warm start)三种。

冷启动(Cold start)

冷启动是指应用程序从头开始:系统的进程在此开始之前没有创建应用程序。冷启动发生在诸如自设备启动以来首次启动应用程序或自系统终止应用程序以来。

在冷启动开始时,系统有三个任务。这些任务是: 1、加载并启动应用程序 2、启动后立即显示应用程序的空白启动窗口 3、创建应用程序进程

当系统为我们创建了应用进程之后,开始创建应用程序对象。

1、启动主线程 2、创建主Activity 3、加载布局 4、屏幕布局 5、执行初始绘制

应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户可以开始使用该应用程序。至此启动完成。

Application创建

当Application启动时,空白的启动窗口将保留在屏幕上,直到系统首次完成绘制应用程序。此时,系统进程会交换应用程序的启动窗口,允许用户开始与应用程序进行交互。这就是为什么我们的程序启动时会先出现一段时间的黑屏(白屏)。

如果我们有自己的Application,系统会onCreate()在我们的Application对象上调用该方法。之后,应用程序会生成主线程(也称为UI线程),并通过创建主要活动来执行任务。

从这一点开始,App就按照他的 应用程序生命周期阶段进行。

Activity创建

应用程序进程创建活动后,活动将执行以下操作:

初始化值。调用构造函数。调用回调方法,例如 Activity.onCreate()),对应Activity的当前生命周期状态。

通常,该 onCreate())方法对加载时间的影响最大,因为它以最高的开销执行工作:加载和膨胀视图,以及初始化活动运行所需的对象。

热启动(Hot start)

应用程序的热启动比冷启动要简单得多,开销也更低。在一个热启动中,系统都会把你的Activity带到前台。如果应用程序的Activity仍然驻留在内存中,那么应用程序可以避免重复对象初始化、布局加载和渲染。

热启动显示与冷启动方案相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。

温启动(Warm start)

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

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频 如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2019-2021字节跳动Android面试历年真题解析》

自行下载直达领取链接:【GitHub】 以上进阶BATJ大厂学习资料可以免费分享给大家,需要完整版的朋友,【点这里可以看到全部内容】。

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-1vWGLRyw-1710772550006)]

《2019-2021字节跳动Android面试历年真题解析》

[外链图片转存中…(img-iEUCOy86-1710772550006)]

自行下载直达领取链接:【GitHub】 以上进阶BATJ大厂学习资料可以免费分享给大家,需要完整版的朋友,【点这里可以看到全部内容】。

精彩链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。