React Native 代码阅读(三):给 Java 对象添加析构机制(Android)

2019-02-01

介绍

我们知道在 Java 中是没有析构函数这个机制的。在学习 React Native 代码的时候,我发现他实现了一种机制,给 Java 对象添加了析构函数的机制。

在本文中我们一探究竟。

DestructorThread

介绍

我们从 DestructorThread 入手,它内部包含一条线程,他会在对象被垃圾回收时执行一个析构的机制。这是给 Java 对象添加了一个析构方法。

从中我们可以理解:我们启动一条线程,它监听 JVM 的垃圾回收过程,hook 住对象被回收的时机,并在对象被回收前触发回调,这个回调就是对象的析构函数。

DestructorThread.Destructor

Destructor 是对象的析构器,它内部含有析构函数。需要析构特性的对象,需要创建一个 DestructorThread.Destructor 的静态子类,一旦引用对象被垃圾回收,DestructorThread 就会调用到 Destructor#destruct() 方法。

它的实现如下:

public abstract static class Destructor extends PhantomReference<Object> {

    private Destructor next;
    private Destructor previous;

    Destructor(Object referent) {
        super(referent, sReferenceQueue);
        sDestructorStack.push(this);
    }

    private Destructor() {
        super(null, sReferenceQueue);
    }

    /** Callback which is invoked when the original object has been garbage collected. */
    abstract void destruct();
}

前面我们说到,监听 JVM 的垃圾回收过程,hook 住对象被回收的时机,这是怎么实现的呢?答案就在这里,我们使用了 PhantomReference(虚引用)。虚引用是 Java 对象引用方式的一种,由于平时较少用到所以很多人可能不太了解,它的作用就是给一个对象释放的 hook 时机。需要注意的是,它必须要同 ReferenceQueue 一起使用。

其中:

  • 在构造函数中传入 sReferenceQueue,这是 ReferenceQueue 实例,在下一节中会介绍。
  • 同时还会向 sDestructorStack 中入栈自己,在下一节中会介绍。
  • 同时还有一个 destruct 抽象方法,这个就是析构函数。

除此之外,我们还注意到 Destructor 自身是一个链表结构,它持有前后对象的引用。

静态区

我们来看 DestructorThread 的静态区:

static {
    sDestructorStack = new DestructorStack();
    sReferenceQueue = new ReferenceQueue();
    sDestructorList = new DestructorList();
    sThread = new Thread("HybridData DestructorThread") {
        @Override
        public void run() {
        while (true) {
            try {
            Destructor current = (Destructor) sReferenceQueue.remove();
            current.destruct();

            if (current.previous == null) {
                sDestructorStack.transferAllToList();
            }

            DestructorList.drop(current);
            } catch (InterruptedException e) {}
        }
        }
    };

    sThread.start();
}

这里初始化了几个成员,他们的类型分别为:

private static DestructorList sDestructorList;
private static DestructorStack sDestructorStack;
private static ReferenceQueue sReferenceQueue;
private static Thread sThread;

下面我们来分析这个过程。

ReferenceQueue

ReferenceQueue 是一种特殊的队列,称为引用队列。

它的使用方法是这样的:

ReferenceQueue sReferenceQueue = new ReferenceQueue()

// ...

while(true) {
    try {
        Object o = sReferenceQueue.remove();
    } catch (InterruptedException e) {}
}

其中:

  • 前面我们在建立虚引用的时候传入了 sReferenceQueue,这些虚引用会放入 ReferenceQueue 中进行跟踪
  • 这个 remove 方法是阻塞式的,直到有对象被内存回收时才会吐出对象来
  • 由于这个过程是阻塞式的,因此要单独放到一个线程中去,这也是 DestructorThread 内部包含一个线程的原因

DestructorStack

我们来看 DestructorStack 的实现:

/** This is a thread safe, lock-free Treiber-like Stack of Destructors. */
private static class DestructorStack {
    private AtomicReference<Destructor> mHead = new AtomicReference<>();

    public void push(Destructor newHead) {
        Destructor oldHead;
        do {
        oldHead = mHead.get();
        newHead.next = oldHead;
        } while (!mHead.compareAndSet(oldHead, newHead));
    }

    public void transferAllToList() {
        Destructor current = mHead.getAndSet(null);
        while (current != null) {
            Destructor next = current.next;
            sDestructorList.enqueue(current);
            current = next;
        }
    }
}

其中:

  • 这里实现了一个并发栈,具体参考 Treiber stack
  • 其中 mHead 指向栈顶,每次 Push 都修改内部 next 指向
  • transferAllToList 的作用是将栈中数据都存入 sDestructorList 中

DestructorList

DestructorList 的作用是持有所有内存中活跃的析构器(析构函数是以类实例的形式存在的)。

它是一个静态类,我们看下它的实现:

/** A doubly-linked list of Destructors. */
private static class DestructorList {
    private Destructor mHead;

    public DestructorList() {
        mHead = new Terminus();
        mHead.next = new Terminus();
        mHead.next.previous = mHead;
    }

    public void enqueue(Destructor current) {
        current.next = mHead.next;
        mHead.next = current;

        current.next.previous = current;
        current.previous = mHead;
    }

    private static void drop(Destructor current) {
        current.next.previous = current.previous;
        current.previous.next = current.next;
    }
}

其中,Terminus 是析构器的一个默认实现:

private static class Terminus extends Destructor {
    @Override
    void destruct() {
        throw new IllegalStateException("Cannot destroy Terminus Destructor.");
    }
}

这个类执行了入队和出队操作。

工作原理

前面我们将各个部分分别进行了学习,在本节中结合起来分析。

在 Destructor 构造的时候,DestructorThread 的静态区会被执行,DestructorList、DestructorStack、ReferenceQueue 和线程都被初始化。

在 Destructor 的构造函数中:

private Destructor() {
    super(null, sReferenceQueue);
    sDestructorStack.push(this);
}

会建立虚引用,并绑定到 sReferenceQueue 中。

同时会向栈中入栈,我们来看第一次入栈时的 DestructorStack.push 的情景:

  • 对象的 next 指向 oldHead(此时是空的)
  • mHead 指向我们的新对象
  • 此时对象的 mPrevious 还是空的

这样如果对象被释放,我们起的线程会有反应:

sThread = new Thread("HybridData DestructorThread") {
    @Override
    public void run() {
    while (true) {
        try {
        Destructor current = (Destructor) sReferenceQueue.remove();
        current.destruct();

        if (current.previous == null) {
            sDestructorStack.transferAllToList();
        }

        DestructorList.drop(current);
        } catch (InterruptedException e) {
        // Continue. This thread should never be terminated.
        }
    }
    }
};

其中:

  • 首先会调用 current 的析构函数
  • 之后会将栈中的内容都转到列表中
  • 再从列表中丢掉当前对象

外部使用

介绍

在上一节中我们分析了 DestructorThread 内部的工作原理。在本节中我们来看是如何使用这个析构机制的。

在 React Native 中,使用到这个机制的类是 HybridData 类。我们看下它的实现:

public class HybridData {

  static {
    SoLoader.loadLibrary("fb");
  }

  private Destructor mDestructor = new Destructor(this);

  public synchronized void resetNative() {
    mDestructor.destruct();
  }

  public boolean isValid() {
    return mDestructor.mNativePointer != 0;
  }

  public static class Destructor extends DestructorThread.Destructor {

    private long mNativePointer;

    Destructor(Object referent) {
      super(referent);
    }

    @Override
    void destruct() {
      deleteNative(mNativePointer);
      mNativePointer = 0;
    }

    static native void deleteNative(long pointer);
  }
}

其中:

  • 在创建 HybridData 实例时,创建 mDestructor 成员,并将自己传入 Destructor。
  • 这样在 Destructor 中建立了对 HybridData 的虚引用
  • 当外界没有对 HybridData 实例的引用后,GC 会将其回收,此时会触发我们 DestructorThread 中的线程
  • 我们从 ReferenceQueue remove 得到虚引用的对象(Destructor),并执行它的 destruct 方法,这个方法在基类中是一个抽象方法,HybridData.destruct 实现了这个方法
  • HybridData.destruct 具体的逻辑是:调用 Native 侧的 deleteNative 可以推导出是删除 Native 侧的对象,之后清空 native 侧的指针
  • 这些都完成后 HybridData 就被彻底从 JVM 中回收了

HybridData

HybridData 这个类在 ReactNative 中广泛被使用,它是一种特殊的对象,虽然它是一个 Java 对象,但它同时引用这一个 Native 对象,并且在 Java 对象回收的时候,Native 也同时回收。

CatalystInstanceImpl 中的 HybridData

  1. 介绍

    下面我们来看,在 CatalystInstanceImpl 中是如何使用 HybridData 的。

    在 CatalystInstanceImpl 中有两个成员:

    private final HybridData mHybridData;
    private native static HybridData initHybrid();
    

    在构造函数中:

    mHybridData = initHybrid();
    

    我们可以看出,在构函数中,CatalystInstanceImpl 调用了原生的 initHybrid 方法,并将返回值存入 mHybridData。

    下面我们进入 Native 层,看看 initHybrid 的实现。

  2. Native 层 initHybrid

    在 ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp 的 CatalystInstanceImpl::initHybrid,对应实现为:

    jni::local_ref<CatalystInstanceImpl::jhybriddata> CatalystInstanceImpl::initHybrid(
        jni::alias_ref<jclass>) {
      return makeCxxInstance();
    }
    

    其中 makeCxxInstance 定义在 ReactAndroid/src/main/jni/first-party/fb/include/fb/fbjni/Hybrid.h 中:

    template<typename T>
    local_ref<HybridDestructor> getHolder(T t) {
      static auto holderField = t->getClass()->template getField<HybridDestructor::javaobject>("mDestructor");
      return t->getFieldValue(holderField);
    }
    
    template<typename T>
    void setNativePointer(T t, std::unique_ptr<detail::BaseHybridClass> new_value) {
      getHolder(t)->setNativePointer(std::move(new_value));
    }
    
    static local_ref<detail::HybridData> makeHybridData(std::unique_ptr<T> cxxPart) {
      auto hybridData = detail::HybridData::create();
      setNativePointer(hybridData, std::move(cxxPart));
      return hybridData;
    }
    
    template <typename... Args>
    static local_ref<detail::HybridData> makeCxxInstance(Args&&... args) {
      return makeHybridData(std::unique_ptr<T>(new T(std::forward<Args>(args)...)));
    }
    

    这段 C++ 代码看起来想当费劲,大体可以知道:

    • Java 层 makeCxxInstance(); 时并没有传入参数,因此 args 是空的
    • 在 CatalystInstanceImpl 中 initHybrid 是一个静态方法
    • 通过我强大的脑补能力,我觉得这里是创建了一个 Native 层 CatalystInstanceImpl 的实例,同时被 Wrap 成一个 HybridData 对象,用于 Java 层使用
    • 这样的好处是,Native 层 CatalystInstanceImpl 就像一个 Java 对象,在没有引用后也可以被自动回收

结论

在本文中,我们分析了 React Native 中,给 Java 添加析构机制,其实质是使用了 Java 的虚引用特性。

同时我们也分析了 React Native 中使用这一机制的 HybridData 类,并用 CatalystInstanceImpl 为例,分析了它的实际用法。