有什么手段可以减少APK体积

要回答这个问题,首先需要了解APK体积增大的原因,我们从这张经典的APK构建流程图开始分析。

apk_build
apk_build

可以看到最终生成的 APK 里面,主要包含三部分内容:Java 源码编译出来的.dex文件,编译后的资源文件 & 未经编译的assets目录,以及一些经 NDK 编译后生成的.so文件。下面针对这三方面给出优化的建议。

dex 文件

dex 文件包含了所有 Java 类编译出来类文件。从“数量”与“质量”两方面入手,我们可以削减类的数目,还可以减少类文件的大小。

开启 Proguard

这样可以在打包时去除没有使用到的类,以及缩短类和字段、方法的命名,从而减少 class 文件大小。需要注意的是,把要保留命名(比如反射)的类 keep 住。对于混淆后打包出来的 APK 应当进行充分的回归测试。

选择较小的适用于移动平台的第三方类库

既可以减少了包体 size,也能减少方法数,从而避免 65535 问题。比如在接入 JSON 解析库时,比较常见的几种类库有:Gson(2.8.5,235Kb)、Jackson(2.9.5,三个 Jar,共 1.7Mb)、Fastjson(1.2.47,533Kb),在满足需求的前提下选择包体最小的 Gson。

定制第三方库

对于开源的第三方库,如果项目里只用到其中 10% 甚至更少的功能,不妨仅将用到的源码拷贝至自己项目里,而不是通过 gradle 完整引入。

资源文件

主要是图片、音视频等,处理不当的话一张图片可能就会增加 1Mb 的体积,因此必须谨慎对待资源文件,不要不加考虑地全盘接受设计师给出的切图。

压缩大小

大部分 png 图片是可以压缩的,你可以使用https://compresspng.com/在线进行压缩。对于音视频,也要在保证效果的前提下,尽量缩减体积。

使用更易扩展的图片格式

使用矢量图、.9 图代替高清切图,尤其是规则形状的背景、边框图片等。

使用 WebP 格式

WebP格式是有损压缩(像JPEG)且有透明通道(像PNG),且压缩率高于JPEG或PNG。在Android Studio中,能将BMP,JPG,PNG或者静态GIF图片转换成WebP格式。

使用WebP文件格式也有一些缺点。第一,低于Android 3.2的版本不支持WebP,第二,WebP的解码时间比PNG长。

用代码代替图片

使用属性动画,而非帧动画。帧动画通常需要多张图片组合才能进行播放,此时通过代码实现缩放、旋转等动画是更好的选择;用 RotateDrawable 代替仅仅是方向不同的“内容相同”的图片;用 layer-list 来制作多层图片从而达到复用。

删除不再使用的资源文件

随着版本迭代,一些旧日需求引入的资源文件将不再使用,此时应当将它们删除。Android Studio 自带的 Lint 工具可以帮我们完成这件事。在 AS 的菜单中选择 Analyze -> Inspect Code。分析需要一定时间,待分析完成后会在窗口展示结果。其中 Unused resources 即是未使用到的资源文件。

不要忘了清理 assets 文件夹下不再使用的文件。

unused_res
unused_res

删除资源是根治的方法,如果你想偷点懒,可以通过 shrinkResources 属性让编译器打包时自动剔除不再使用的资源,该属性需要与 proguard 同时开启。

1
2
3
4
5
6
7
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

如果想看看在激活自动缩减资源后 APK 缩减了多少,可以运行 shrinkReleaseResources 任务,这个任务会打印出包的大小缩减了多少。

自动缩减资源有一个问题:它可能移除了过多的资源,特别是那些动态使用的资源肯定会被删除。为了防止这种情况,可以在 res/raw/ 下的 keep.xml 文件中定义这些例外。

放弃一些图片资源

Android 有 ldpi、mdhi、hdpi、xhdpi、xxhdpi、xxxhdpi 等多种分辨率格式,谨慎的人也许会针对分辨率提供一份切图,但我在这里像你建议,千万别这么做。

  • 一方面,Android 系统提供了兼容的处理方案,比如会把 hdpi 的图片缩放到 ldpi 使用。
  • 另一方面,在添加资源文件时应当考虑它所对应机型的占有率。比如绝大部分情况下不需要准备 ldpi 和 xxxhdpi 的图片,前者的手机早已过时,后者则是给 2K 屏幕使用的,目前市面上并不常见,也可以选择忽略。

对于第三方 aar 引入的资源文件,也可以指定引入特定分辨率的,通过 gradle 配置实现这一点。

1
2
3
4
5
6
defaultConfig {
// ...

resConfigs "en", "de", "fr", "it" // 指定语言
resConfigs "hdpi", "xhdpi", "xxhdpi" // 指定显示密度
}

so 文件

当我们项目里需要使用 NDK 时,会将编译生成的 .so 文件置于项目里面,当你解压一个 APK 时会发现它们的身影。有以下方法可以缩减它们所占的体积。

使用兼容指令集

处理器的指令集通常是向下兼容的,比如[TODO]的指令集就可以兼容[TODO],意味着你可以只提供一份[TODO]的 so(但我不建议这么做,因为更高版本的指令集可以提供更高的运行效率)。

去除完全用不到的指令集文件

比如 x86、x86_64,现在极少手机是使用 x86 的 CPU 架构了,你自然可以去除它们。需要注意的是,如果你想让 APP 在模拟器上运行,仍需要保留它们。

在 gradle 文件的 defaultConfig 域下配置需要的 so:

1
2
3
4
5
6
7
defaultConfig {
// ... ...
ndk {
//设置支持的SO库架构
abiFilters 'arm64-v8a', 'armeabi' //, 'x86', ,'x86_64', 'armeabi-v7a
}
}

其它手段

插件化

插件化技术自 2015 年井喷后,一直是 Android 面试中长盛不衰的面试题。发布 APP 时只发布包含必要功能的宿主,子模块功能以插件的形式下发。也是一个有效降低 APK 体积的方法。但提高了项目的开发难度和维护成本,同时还需要一个成熟的发布后台。

APK 分割

可以通过在 gradle 配置中定义一个 splits 代码块来配置分割,目前支持 density 分割和 ABI 分割。比如你可以通过配置,在一次打包中生成以下 APK,进而将它们分别发布给不同机型。

density 分割

  • app-hdpi-release.apk
  • app-universal-release.apk
  • app-xhdpi-release.apk
  • app-xxhdpi-release.apk
  • app-xxxhdpi-release.apk

ABI 分割

  • app-armeabi-v7a-debug.apk
  • app-mips-debug.apk
  • app-x86-debug.apk

模块化

这个 Title 太大,我会单独撰文加以讲解。

插件化

这个 Title 太大,我同样会单独撰文加以讲解。上个月刚读完包建强所著的《Android 插件话开发指南》一书,所获颇丰。后面我会将书中的重点内容总结成文字笔记分享在博客里面。

GET 和 POST 区别

本质上无区别

首先,GET 和 POST 都是HTTP 协议中发送请求的方法,这两者底层的协议也都是 TCP/IP,这两者并没有本质上的区别。

从底层原理上,GET 产生了一个 TCP 数据包,POST 产生了两个 TCP 数据包。

对于 GET 请求,浏览器把 HTTP Header 和 Data 一同发送出去,服务器相应 200。

对于 POST 请求,浏览器 先发送 HTTP Header,服务器响应 100(continue),浏览器再发送 Data,服务器响应 200(返回数据)。

是否可以互换

那么是否可以把所有的 POST 请求都改成 GET,以提高网站相应效率呢?答案是不行。

  1. 两者具有不同的语义。
  2. 在网络环境好的情况下,两次请求与一次请求在响应时间上相差微乎其微;在网络情况差的情况下,两次请求更有助于进行数据完整性校验。
  3. 并非所有浏览器在 POST 时都发送两个包,Firefox 就只发送一次。

其它一些补充

  • 浏览器回退:GET在浏览器回退时是无害的(幂等性),而POST会再次提交请求。
  • Bookmark:GET产生的URL地址可以被Bookmark,而POST不可以。
  • Cache:GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • 编码:GET请求只能进行url编码,而POST支持多种编码方式。
  • 参数保存:GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • 参数长度:GET请求在URL中传送的参数是有长度限制的,而POST没有。
  • 参数类型:对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • 参数安全:GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • 参数传递:GET参数通过URL传递,POST放在Request body中。

LeakCanary 检查内存泄漏的原理

背景知识

JVM 运行时内存分区,内存模型,垃圾回收算法,GC Roots,强软弱虚引用。

A small leak will sink a great ship.
– Benjamin Franklin

LeakCanary 是由Square公司开源的内存泄漏检测工具。Logo 是一只小黄鸡。

LeakCanaryLogo
LeakCanaryLogo

基本原理

  1. 在 Activity.onDestroy 方法里手动触发 GC。
  2. 利用 ReferenceQueue + WeakReference 判断是否有未释放的引用。
  3. 结合 dump memory 得到的 hprof 文件,利用 HaHa(Headless Android Heap Analyzer) 分析出泄漏位置。

源码分析

整体流程

leakcanary整体流程
leakcanary整体流程

入口

需要在 Application 类中启用 LeakCanary。

1
2
3
4
5
6
7
8
9
10
11
// 安装
if (!LeakCanary.isInAnalyzerProcess(WeiboApplication.this)) {
LeakCanary.install(WeiboApplication.this);
}

// install
public static RefWatcher install(Application application) {
return ((AndroidRefWatcherBuilder) refWatcher(application)
.listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build())) //配置监听器及分析数据格式
.buildAndInstall();
}

LeakCanary 会运行在两个进程:App进程中运行监听任务,工作进程中运行分析任务。

监听

install方法里,创建了一个RefWatcher对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public RefWatcher buildAndInstall() {
RefWatcher refWatcher = this.build();
if(refWatcher != RefWatcher.DISABLED) {
LeakCanary.enableDisplayLeakActivity(this.context);
ActivityRefWatcher.install((Application)this.context, refWatcher);
}

return refWatcher;
}

public static void install(Application application, RefWatcher refWatcher) {
(new ActivityRefWatcher(application, refWatcher)).watchActivities();
}

private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
public void onActivityStarted(Activity activity) {}
public void onActivityResumed(Activity activity) {}
public void onActivityPaused(Activity activity) {}
public void onActivityStopped(Activity activity) { }
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}

public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};

void onActivityDestroyed(Activity activity) {
this.refWatcher.watch(activity);
}

LeakCanary 通过Application.registerActivityLifecycleCallbacks方法,注册了 Activity 生命周期的监听,在监测到onDestroyed调用时,触发RefWatcher.watch方法。下面是该方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void watch(Object watchedReference, String referenceName) {
if (this != DISABLED) {
Preconditions.checkNotNull(watchedReference, "watchedReference");
Preconditions.checkNotNull(referenceName, "referenceName");
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();//保证key的唯一性
this.retainedKeys.add(key);
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}


final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;

KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) { //ReferenceQueue类监听回收情况
super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
this.key = (String)Preconditions.checkNotNull(key, "key");
this.name = (String)Preconditions.checkNotNull(name, "name");
}
}

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}

KeyedWeakReferenceWeakReference,这里利用了ReferenceQueue来监听 GC 后的回收情况。ReferenceQueue的原理是,当 GC 检测到对象生命周期结束时,会将其添加到 ReferenceQueue 中。当 GC 过后对象一直不被加入 ReferenceQueue,说明它可能存在内存泄漏。

正是利用 ReferenceQueue 这一特性,LeakCanary 实现了对对象是否被释放的监控。

监测 Fragment 泄漏

上文中看到只在 Activity.onDestroy 中进行检测,如果需要检测 Fragment 时,应当手动在 Fragment.onDestroy 中创建一个 RefWatcher 对象,并调用 watch 方法。

1
2
3
4
5
6
7
public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

ensureGone

是检测回收的核心代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
this.removeWeaklyReachableReferences(); //先将引用尝试从队列中poll出来
if(this.debuggerControl.isDebuggerAttached()) { //规避调试模式
return Result.RETRY;
} else if(this.gone(reference)) { //检测是否已经回收
return Result.DONE;
} else { //如果没有被回收,则手动GC
this.gcTrigger.runGc();//手动GC方法
this.removeWeaklyReachableReferences();//再次尝试poll,检测是否被回收
if(!this.gone(reference)) { // 还没有被回收,则dump堆信息,调起分析进程进行分析
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if(heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;//需要重试
}

long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
}

return Result.DONE;
}
}

private boolean gone(KeyedWeakReference reference) {
return !this.retainedKeys.contains(reference.key);
}

private void removeWeaklyReachableReferences() {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
this.retainedKeys.remove(ref.key);
}
}

ensureGone方法通过检测referenceQueue队列的引用情况,来判断回收情况,通过手动 GC 来进一步确认回收情况。这是一个耗时过程,运行在WatchExecutor中。

LeakCanary 在主线程空闲时候执行检测任务,代码位于AndroidWatchExecutor中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public final class AndroidWatchExecutor implements WatchExecutor {
static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final Handler backgroundHandler;
private final long initialDelayMillis;
private final long maxBackoffFactor;

public AndroidWatchExecutor(long initialDelayMillis) {
HandlerThread handlerThread = new HandlerThread("LeakCanary-Heap-Dump");
handlerThread.start();
this.backgroundHandler = new Handler(handlerThread.getLooper());
this.initialDelayMillis = initialDelayMillis;
this.maxBackoffFactor = 9223372036854775807L / initialDelayMillis;
}

public void execute(Retryable retryable) {
if(Looper.getMainLooper().getThread() == Thread.currentThread()) {
this.waitForIdle(retryable, 0);//需要在主线程中检测
} else {
this.postWaitForIdle(retryable, 0);//post到主线程
}

}

void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
this.mainHandler.post(new Runnable() {
public void run() {
AndroidWatchExecutor.this.waitForIdle(retryable, failedAttempts);
}
});
}

void waitForIdle(final Retryable retryable, final int failedAttempts) {
Looper.myQueue().addIdleHandler(new IdleHandler() {
public boolean queueIdle() {
AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);//切换到子线程
return false;
}
});
}

void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long)Math.min(Math.pow(2.0D, (double)failedAttempts), (double)this.maxBackoffFactor); // 二进制退让算法
long delayMillis = this.initialDelayMillis * exponentialBackoffFactor;
this.backgroundHandler.postDelayed(new Runnable() {
public void run() {
Result result = retryable.run();//RefWatcher.this.ensureGone(reference, watchStartNanoTime)执行
if(result == Result.RETRY) {
AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
}

}
}, delayMillis);
}
}

其中调用了MessageQueue.addIdleHandler方法,Looper 中的 MessageQueue 有个mIdleHandlers队列,在获取下个要执行的 Message 时,如果没有发现可执行的 Message,就会回调queueIdle()方法,如果queueIdle()返回false,则移除该 IdleHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Message next() {
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
···
···//省略部分消息查找代码

if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
···

return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}


// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {//返回false,则从队列移除,下次空闲不会调用。
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

分析

利用 VMDebug+HaHa 完成分析任务。

  1. 在后台线程检查引用是否被清除,如果没有,调用 GC。
  2. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
  3. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
  4. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
  5. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
  6. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

HeapAnalyzerService

是一个 IntentService,调用 HAHA 中的HeapAnalyzer对 hprof 文件进行分析,找出泄露点。由于运行在不同进程,通过 Intent 传递数据。最终将结果发回给监听器。

1
2
3
4
5
6
7
8
9
10
11
@Override protected void onHandleIntent(Intent intent) {
if (intent == null) {// intent 为空直接返回
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);//获取回调类的类名
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);//获取 HeapDump
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);//创建 HeapAnalyzer
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);//检查泄漏(通过 HAHA 来完成),并获取结果
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);//将分析结果发送给监听器
}

参考

掘金:Java 内存问题及 LeakCanary 原理分析
拆轮子系列——LeakCanary工作原理

创建一个对象时内部流程

Java 在 new 一个对象的时候,会先检查对象所属的类是否已经加载到内存。如果没有加载,则会先执行类的加载过程;如果已经加载,则直接执行对象的创建过程

类的加载过程

Java 使用双亲委派模型来进行类的加载。

如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。

这样做的好处是能够确保一个类的全局唯一性。因为类的唯一性由加载器+类名共同决定。使用双亲委派模型保证了同一个类始终由同一加载器进行加载。

类加载过程
类加载过程

1. 加载

由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到 JVM 内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的 java.lang.Class 对象实例。

2. 验证

  • 格式验证:验证是否符合class文件规范。
  • 语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)。
  • 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)。

3. 准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值;被final修饰的static变量(常量),会直接赋值。

4. 解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。

解析需要静态绑定的内容。(所有不会被重写的方法和域都会被静态绑定)

以上 2、3、4 三个阶段又合称为链接阶段,链接阶段要做的是将加载到 JVM 中的二进制字节流的类数据信息合并到 JVM 的运行时状态中。

5. 初始化(父父子子)

5.1 赋值静态变量。

5.2 执行静态代码块。

因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样

最终,方法区会存储当前类类信息,包括类的静态变量类初始化代码定义静态变量时的赋值语句 静态初始化代码块)、实例变量定义实例初始化代码定义实例变量时的赋值语句实例代码块构造方法)和实例方法,还有父类的类信息引用

对象创建过程

1. 在堆区分配对象需要的内存

包括本类与父类所有实例变量,不包括静态变量。

2. 为实例变量赋默认值

将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值。

3. 执行实例初始化代码

初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法

如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前

如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它。

需要注意的是,每个子类对象持有父类对象的引用,可在内部通过 super 关键字来调用父类方法,但在外部不可访问。并且子类对象创建时只是调用父类构造函数,并非创建父类对象

Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用。

虚方法表

通过实例引用调用实例方法的时候,先从方法区中对象的实际类型信息找,找不到的话再去父类类型信息中找。

如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要经过很多次查找。这时候大多系统会采用一种称为虚方法表的方法来优化调用的效率。

所谓虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包括该类的对象所有动态绑定的方法及其地址,包括父类的方法,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类。

Demo

实例初始化不一定要在类初始化结束之后才开始初始化

  • 类初始化<clinit>()
  • 实例初始化<init>()

在Java中, 创建一个对象常常需要经历如下几个过程:

  1. 父类的类构造器<clinit>()
  2. 子类的类构造器<clinit>()
  3. 父类的成员变量和实例代码块
  4. 父类的构造函数
  5. 子类的成员变量和实例代码块
  6. 子类的构造函数。

你可以使用 https://www.tutorialspoint.com/compile_java_online.php 进行在线验证。

一道测验题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class StaticTest {
public static void main(String[] args) {
staticFunction();
}

static StaticTest st = new StaticTest();

static { //静态代码块
System.out.println("1");
}

{ // 实例代码块
System.out.println("2");
}

StaticTest() { // 实例构造器
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}

public static void staticFunction() { // 静态方法
System.out.println("4");
}

int a = 110; // 实例变量
static int b = 112; // 静态变量
}
/* Output:
2
3
a=110,b=0
1
4
*/

另一道测验题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Foo {
int i = 1;

Foo() {
System.out.println(i);
int x = getValue();
System.out.println(x);
}

{
i = 2;
}

protected int getValue() {
return i;
}
}

//子类
class Bar extends Foo {
int j = 1;

Bar() {
j = 2;
}

{
j = 3;
}

@Override
protected int getValue() {
return j;
}
}

public class ConstructorExample {
public static void main(String... args) {
Bar bar = new Bar();
System.out.println(bar.getValue());
}
}
/* Output:
2
0
2
*/

参考