1 问题描述
1.1 “null” or “Activity实例引用”
请阅读如下一段代码,思考:TestStatic.getActivity() 返回值是 “null” 还是 “Activity的实例引用”?
public class TestStatic {
private static TestStatic sInstance = new TestStatic();
private static Activity sActivity = null;
private TestStatic() {
sActivity = new Activity();
}
public static Activity getActivity() {
return sActivity;
}
}
1.2 问题分析
这个问题主要涉及了虚拟机内部类初始化过程知识,参考:[类加载链接、初始化、实例化](http://www.jianshu.com/p/0bff0413fd2f)
类初始过程包括:类加载、链接、初始化、实例化。这里主要关注 初始化 阶段,初始化 阶段主要执行 静态代码块,初始化静态域成员,这两个操作都是在类初始化方法
有分析步骤如下:
- 调用 TestStatic 类的静态方法 getActivity(),导致 TestStatic 类初始化,执行初始化方法
; - 在初始化方法中初始化静态域成员 sIntance , 并导致执行 TestStatic 的构造方法,在构造方法中实例化 Activity,并将对象引用保存在静态域成员 sActivity;
- 接下来继续执行
, 将 *** sActivity*** 赋为 null, 执行完毕; - 所以,最后静态方法 getActivity() 返回 sActivity 为 null。
如果你的分析过程也是这样,那么恭喜你 答案 是正确的,但不要高兴太早,因为 分析过程是错误 的。如果认真读了 [类加载链接、初始化、实例化](http://www.jianshu.com/p/0bff0413fd2f), 就会发现问题发生在第 2 步骤的分析。
private static TestStatic sInstance = new TestStatic();
这里实例化 TestStatic 类时,new 的操作也会导致 TestStatic 类的初始化,因为
.method static constructor ()V
.registers 1
.prologue
00000000 new-instance v0, TestStatic
00000004 invoke-direct TestStatic->()V, v0
0000000A sput-object v0, TestStatic->sInstance:TestStatic
0000000E const/4 v0, 0x0
00000010 sput-object v0, TestStatic->sActivity:Activity
00000014 return-void
.end method
.method private constructor ()V
.registers 2
.prologue
00000000 invoke-direct Object->()V, p0
00000006 new-instance v0, Activity
0000000A invoke-direct Activity->()V, v0
00000010 sput-object v0, TestStatic->sActivity:Activity
00000014 return-void
.end method
00000000 new-instance v0, TestStatic
new-instance 会触发 TestStatic 类的初始化,即在
当然不是, 那真相是怎样子的呢?
这个问题的背后是隐藏了一个关于 类初始化 很关键的知识点,接下来完整分析下这个过程。
2 类初始化
关于引起类的初始化的条件可参考 [类加载链接、初始化、实例化](http://www.jianshu.com/p/0bff0413fd2f), 就会发现问题发生在第 2 步骤的分析, 这里不再赘述。挑与本问题相关的2种条件进行分析。
- 调用 静态方法:字节码为:invoke-static;
- 实例化类,字节码为:new-instance。
2.1 invoke-static 字节码触发的类初始化
虚拟机(Dalvik)将该字节码解释成如下代码块执行(省略不相关部分):
GOTO_TARGET(invokeStatic, bool methodCallRange)
EXPORT_PC();
...
methodToCall = dvmDexGetResolvedMethod(methodClassDex, ref);
if (methodToCall == NULL) {
methodToCall = dvmResolveMethod(curMethod->clazz, ref, METHOD_STATIC);
if (methodToCall == NULL) {
ILOGV("+ unknown method");
GOTO_exceptionThrown();
}
...
}
...
GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst);
GOTO_TARGET_END
dvmDexGetResolvedMethod 先从odex文件中找到被调用静态方法,dvmResolveMethod() 会判断 被调用静态方法所属的类 是否已经正确加载,初始化了。否则,触发对该类的加载,初始化等操作。
Method* dvmResolveMethod(const ClassObject* referrer, u4 methodIdx,
MethodType methodType)
{
...
resClass = dvmResolveClass(referrer, pMethodId->classIdx, false);
...
if (methodType == METHOD_DIRECT) {
resMethod = dvmFindDirectMethod(resClass, name, &proto);
} else if (methodType == METHOD_STATIC) {
resMethod = dvmFindDirectMethodHier(resClass, name, &proto);
} else {
resMethod = dvmFindVirtualMethodHier(resClass, name, &proto);
}
...
/*
* If we're the first to resolve this class, we need to initialize
* it now. Only necessary for METHOD_STATIC.
*/
if (methodType == METHOD_STATIC) {
if (!dvmIsClassInitialized(resMethod->clazz) &&
!dvmInitClass(resMethod->clazz))
{
assert(dvmCheckException(dvmThreadSelf()));
return NULL;
} else {
assert(!dvmCheckException(dvmThreadSelf()));
}
} else {
/*
* Edge case: if the for a class creates an instance
* of itself, we will call on a class that is still being
* initialized by us.
*/
assert(dvmIsClassInitialized(resMethod->clazz) ||
dvmIsClassInitializing(resMethod->clazz));
}
...
}
调用 dvmIsClassInitialized() 判断类是否已经正确初始化,通过判断类的 status 是否已经处于CLASS_INITIALIZED 状态。
/*
* Determine if a class has been initialized.
*/
INLINE bool dvmIsClassInitialized(const ClassObject* clazz) {
return (clazz->status == CLASS_INITIALIZED);
}
若类没初始化,则调用 dvmInitClass()方法初始化类。
bool dvmInitClass(ClassObject* clazz)
{
...
dvmLockObject(self, (Object*) clazz);
...
while (clazz->status == CLASS_INITIALIZING) {
if (clazz->initThreadId == self->threadId) {
//ALOGV("HEY: found a recursive ");
goto bail_unlock;
}
...
/*
* Wait for the other thread to finish initialization. We pass
* "false" for the "interruptShouldThrow" arg so it doesn't throw
* an exception on interrupt.
*/
dvmObjectWait(self, (Object*) clazz, 0, 0, false);
...
if (clazz->status == CLASS_INITIALIZING) {
ALOGI("Waiting again for class init");
continue;
}
...
goto bail_unlock;
}
...
clazz->initThreadId = self->threadId;
android_atomic_release_store(CLASS_INITIALIZING,
(int32_t*)(void*)&clazz->status);
dvmUnlockObject(self, (Object*) clazz);
...
initSFields(clazz);
/* Execute any static initialization code.*/
method = dvmFindDirectMethodByDescriptor(clazz, "", "()V");
if (method == NULL) {
LOGVV("No found for %s", clazz->descriptor);
} else {
LOGVV("Invoking %s.", clazz->descriptor);
JValue unused;
dvmCallMethod(self, method, NULL, &unused);
}
...
bail_unlock:
dvmUnlockObject(self, (Object*) clazz);
return (clazz->status != CLASS_ERROR);
}
理解dvmInitClass()执行过程是我们理解认清这个问题的关键。代码块省略了无关的部分,阅读起来逻辑比较简单清晰。
- 类的初始化通过ClasObject中的一个lock和status状态来处理并发初始化类的问题。
- 第一个进入该方法的人,上锁,其他人无法进来。接着,设置初始化类的线程ID,设置类status为:CLASS_INITIALIZING,解锁。调用initSFields()初始化一些简单静态域,最后看类是否有
方法,有的话则调用。执行完后,则类的初始化步骤完成。 - 在类首次还没初始化完成情况下 有其人 进入该方法,在第2点说明中,解锁 后,会进入 while(clazz->status == CLASS_INITIALIZING) 循环中,分2种情况:
(1)如果是 正在初始化当前类的线程,则 直接退出;
(2)如果是 其他线程,则会 阻塞,直到当前的初始化完成(成功或失败),最后也是直接退出。
基于上面3点的分析,便可以将前面的问题解释清楚了。在
在本次的例子中,属于第1中情况,即 同一个线程中循环调用
而第2中情况,即是多线程同时初始化一个 *Class 时出现,虚拟机选择阻塞,直到初始化操作完成。我们在写java代码的时候,根本不用显示处理这种初始化并发的问题,因为虚拟机帮我们做了。
2.2 new-instance触发的类初始化
new-instance 被虚拟机解释成代码块如下。
HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/)
{
...
clazz = dvmDexGetResolvedClass(methodClassDex, ref);
...
if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz))
GOTO_exceptionThrown();
...
newObj = dvmAllocObject(clazz, ALLOC_DONT_TRACK);
...
}
new-instance指令的核心是为实例对象分配内存空间,而在这个操作之前,必须先保证类已经正确被初始化,否则会调用dvmInitClass()对类进行初始化。
回到例子中,这里有一个知识点值得了解下。
- 正常情况下,new实例一个类后,类的 实例化 是在 类初始化 后面完成。
- 在这个例子中不是,因为 TestStatic 类的 实例化 在其
方法中,执行 new-instance指令。dvmIsClassInitialized() 判断 TestStatic 到还初始化没完成。导致调用dvmInitClass() 对 TestStatic 进行初始化。从上面分析得知,属于 第一种情况,因此这里直接返回。 - 然后执行 dvmAllocObject 给类的 对象 分配内存空间,并调用其构造方法初始化实例对象。
- 因此这里,可能会出现类的 实例对象初始化 在 类初始化 前面完成。
我们写java代码的时候,其实不用担心。因为同个线程中,出现这种情况的时候,会等到 类初始化 完成后才进行后面的操作,而不同的线程,是会阻塞到类初始化完成的。
3 总结
通过上面的分析,总结下这个问题正确的分析思路应该是:
- 调用 TestStatic 类的静态方法 getActivity(),导致 TestStatic 类初始化,执行初始化方法
; - 在初始化方法中 实例化 静态域成员 sIntance , 实例化过程中会再次触发 TestStatic 类的初始化,调到
方法,这次 直接退出。 继续 TestStatic 的实例化,并执行 TestStatic 的构造方法,在构造方法中实例化 Activity,并将对象引用保存在静态域成员 sActivity 中; - 接下来继续执行
, 将 *** sActivity*** 赋为 null, 执行完毕; - 所以,最后静态方法 getActivity() 返回 sActivity 为 null。
从这个问题中,我们也分析了 类初始化 并发的问题。
这个问题是我在工作群上偶然看到的,有人问为什么使用与 sActivity(与此一样的情况) 时返回为空?为了分析透彻而翻查了源码。其实,这个问题只要合理设计下自己的程序,就不会发生了。但我们依然可以从中学到背后的知识。