现代 C++ 跨平台开发-内存篇:多平台跨层调用场景的内存管理

本文是整个【现代 C++ 跨平台开发-内存篇】系列的第 8 篇,主要涉及:多平台跨层调用场景的内存管理。

JNI 内存管理

JNI 的世界里,要摒弃 GC 搞定一切的思维。

引用

  • 本地引用:

    • 只有返回 jobject 的函数才会产生本地引用:

      • 方法和字段查找返回的 jmethodID/jfieldID 等 ID 类型都不会产生本地引用;

      • 类查找返回的 jclass 属于本地引用;

    • 循环或回调等频繁调用场景,本地引用需显式释放;

    • 也可通过 PushLocalFrame()/PopLocalFrame() 自动管理;

  • 全局引用:

    • 用于延长生命周期,需要在合适的时机手动释放。

源码分析

数据结构
class JNIEnvExt : public JNIEnv {
  // Cookie used when using the local indirect reference table.
  jni::LRTSegmentState local_ref_cookie_;

  // JNI local references.
  jni::LocalReferenceTable locals_;

  // Stack of cookies corresponding to PushLocalFrame/PopLocalFrame calls.
  // TODO: to avoid leaks (and bugs), we need to clear this vector on entry (or return)
  // to a native method.
  std::vector<jni::LRTSegmentState> stacked_local_ref_cookies_;
}

// The state of the current segment contains the top index.
struct LRTSegmentState {
  uint32_t top_index;
};

class LocalReferenceTable {
   /// semi-public - read/write by jni down calls.
  LRTSegmentState segment_state_;

  //...

  // The singly-linked list of free nodes.
  // We use entry indexes instead of pointers and `kFreeListEnd` instead of null indicates
  // the end of the list. See `LocalReferenceTable::GetEntry()` and `LrtEntry::GetNextFree().
  //
  // We use the lowest bit to record whether CheckJNI is enabled. This helps us
  // check that the list is empty and CheckJNI is disabled in a single comparison.
  uint32_t free_entries_list_;

  // Individual tables.
  // As long as we have only one small table, we use `small_table_` to avoid an extra load
  // from another heap allocated location, otherwise we set it to null and use `tables_`.
  LrtEntry* small_table_;  // For optimizing the fast-path.
  dchecked_vector<LrtEntry*> tables_;
  //...
};

class LrtEntry {
   // We record the contents as a `GcRoot<>` but it is an actual `GcRoot<>` only if it's below
  // the current segment's top index, it's not a "serial number" or inactive entry in a CheckJNI
  // chunk, and it's not marked as "free". Such entries are never null.
  GcRoot<mirror::Object> root_;
};
类/方法/字段查找
static jclass FindClass(JNIEnv* env, const char* name) {
    //...
    Runtime* runtime = Runtime::Current();
    ClassLinker* class_linker = runtime->GetClassLinker();
    std::string descriptor(NormalizeJniClassDescriptor(name));
    ScopedObjectAccess soa(env);
    ObjPtr<mirror::Class> c = nullptr;
    if (runtime->IsStarted()) {
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader<kEnableIndexIds>(soa)));
      c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
    } else {
      c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
    }
    return soa.AddLocalReference<jclass>(c);
  }

static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig) {
    //...
    ScopedObjectAccess soa(env);
    return FindMethodID<kEnableIndexIds>(soa, java_class, name, sig, false);
  }

static jfieldID GetFieldID(JNIEnv* env, jclass java_class, const char* name, const char* sig) {
    //...
    ScopedObjectAccess soa(env);
    return FindFieldID<kEnableIndexIds>(soa, java_class, name, sig, false);
  }

template<bool kEnableIndexIds>
static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class, const char* name, const char* sig, bool is_static) REQUIRES_SHARED(Locks::mutator_lock_) {
  return jni::EncodeArtMethod<kEnableIndexIds>(FindMethodJNI(soa, jni_class, name, sig, is_static));
}

template<bool kEnableIndexIds>
static jfieldID FindFieldID(const ScopedObjectAccess& soa, jclass jni_class, const char* name, const char* sig, bool is_static) REQUIRES_SHARED(Locks::mutator_lock_) {
  return jni::EncodeArtField<kEnableIndexIds>(FindFieldJNI(soa, jni_class, name, sig, is_static));
}

ArtMethod* FindMethodJNI(const ScopedObjectAccess& soa, jclass jni_class, const char* name, const char* sig, bool is_static) {
  ObjPtr<mirror::Class> c = EnsureInitialized(soa.Self(), soa.Decode<mirror::Class>(jni_class));
  //...
  ArtMethod* method = nullptr;
  auto pointer_size = Runtime::Current()->GetClassLinker()->GetImagePointerSize();
  if (c->IsInterface()) {
    method = c->FindInterfaceMethod(name, sig, pointer_size);
  } else {
    method = c->FindClassMethod(name, sig, pointer_size);
  }
  //...
  return method;
}

ArtField* FindFieldJNI(const ScopedObjectAccess& soa, jclass jni_class, const char* name, const char* sig, bool is_static) {
  //...
  ArtField* field = nullptr;
  ObjPtr<mirror::Class> field_type;
  ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
  //...
  if (sig[1] != '\0') {
    Handle<mirror::ClassLoader> class_loader(hs.NewHandle(c->GetClassLoader()));
    field_type = class_linker->FindClass(soa.Self(), sig, class_loader);
  } else {
    field_type = class_linker->FindPrimitiveClass(*sig);
  }
  //...
  if (is_static) {
    field = c->FindStaticField(name, field_type->GetDescriptor(&temp));
  } else {
    field = c->FindInstanceField(name, field_type->GetDescriptor(&temp));
  }
  //...
  return field;
}

可以看到,类查找会调用 AddLocalReference(),而方法和字段查找不会。

LocalReference 管理
jobject JNIEnvExt::NewLocalRef(mirror::Object* obj) {
  //...
  jobject ref = reinterpret_cast<jobject>(locals_.Add(local_ref_cookie_, obj, &error_msg));
  //...
  return ref;
}

void JNIEnvExt::DeleteLocalRef(jobject obj) {
  if (obj != nullptr) {
    locals_.Remove(local_ref_cookie_, reinterpret_cast<IndirectRef>(obj));
  }
}

IndirectRef LocalReferenceTable::Add(LRTSegmentState previous_state, ObjPtr<mirror::Object> obj, std::string* error_msg) {
  //...
  auto store_obj = [obj, this](LrtEntry* free_entry, const char* tag) {
    free_entry->SetReference(obj);
    IndirectRef result = ToIndirectRef(free_entry);
    //...
    return result;
  };
  //...
  LrtEntry* free_entry = GetEntry(top_index);
  segment_state_.top_index = top_index + 1u;
  return store_obj(free_entry, "slow-path");
}

bool LocalReferenceTable::Remove(LRTSegmentState previous_state, IndirectRef iref) {
  //...
  LrtEntry* entry = ToLrtEntry(iref);
  uint32_t entry_index = GetReferenceEntryIndex(iref);
  //...

  // Prune the free entry list if a segment with holes was popped before the `Remove()` call.
  uint32_t first_free_index = GetFirstFreeIndex();
  if (first_free_index != kFreeListEnd && first_free_index >= top_index) {
    PrunePoppedFreeEntries([&](size_t index) { return GetEntry(index); });
  }

  //...
  if (is_top_entry) {
    // Top-most entry. Scan up and consume holes created with the current CheckJNI setting.
    constexpr uint32_t kDeadLocalValue = 0xdead10c0;
    entry->SetReference(reinterpret_cast32<mirror::Object*>(kDeadLocalValue));

    // TODO: Maybe we should not prune free entries from the top of the segment
    // because it has quadratic worst-case complexity. We could still prune while
    // the first free list entry is at the top.
    uint32_t prune_start = prune_end;
    size_t prune_count;
    auto find_prune_range = [&](size_t chunk_size, auto is_prev_entry_free) {
      while (prune_start > bottom_index && is_prev_entry_free(prune_start)) {
        prune_start -= chunk_size;
      }
      prune_count = (prune_end - prune_start) / chunk_size;
    };

    if (prune_count != 0u) {
      // Remove pruned entries from the free list.
      size_t remaining = prune_count;
      uint32_t free_index = GetFirstFreeIndex();
      while (remaining != 0u && free_index >= prune_start) {
        //...
        LrtEntry* pruned_entry = GetEntry(free_index);
        free_index = pruned_entry->GetNextFree();
        pruned_entry->SetReference(reinterpret_cast32<mirror::Object*>(kDeadLocalValue));
        --remaining;
      }
      free_entries_list_ = FirstFreeField::Update(free_index, free_entries_list_);
      while (remaining != 0u) {
        //...
        LrtEntry* free_entry = GetEntry(free_index);
        while (free_entry->GetNextFree() < prune_start) {
          free_index = free_entry->GetNextFree();
          //...
          free_entry = GetEntry(free_index);
        }
        LrtEntry* pruned_entry = GetEntry(free_entry->GetNextFree());
        free_entry->SetNextFree(pruned_entry->GetNextFree());
        pruned_entry->SetReference(reinterpret_cast32<mirror::Object*>(kDeadLocalValue));
        --remaining;
      }
      //...
    }
    segment_state_.top_index = prune_start;
    //...
  } else {
    // Not the top-most entry. This creates a hole.
    entry->SetNextFree(GetFirstFreeIndex());
    free_entries_list_ = FirstFreeField::Update(entry_index, free_entries_list_);
    //...
  }
  return true;
}

LrtEntry* GetEntry(size_t entry_index) const {
    //...
    size_t table_start_index = (entry_index < kSmallLrtEntries) ? 0u : TruncToPowerOfTwo(entry_index);
    size_t table_index = (entry_index < kSmallLrtEntries) ? 0u : NumTablesForSize(table_start_index);
    LrtEntry* table = tables_[table_index];
    return &table[entry_index - table_start_index];
  }

可见:

  • 新增和释放单个 LocalReference 都是针对当前的 local_ref_cookie_LRTSegmentState 类型,记录栈顶);

  • 并没有直接释放内存,而是标记为无效、加入空闲链表(free list):避免频繁移动内存,用 free list 复用“洞”(holes)。

Frame 管理
void JNIEnvExt::PushFrame(int capacity) {
  DCHECK_GE(locals_.FreeCapacity(), static_cast<size_t>(capacity));
  stacked_local_ref_cookies_.push_back(local_ref_cookie_);
  local_ref_cookie_ = locals_.GetSegmentState();
}

void JNIEnvExt::PopFrame() {
  locals_.SetSegmentState(local_ref_cookie_);
  local_ref_cookie_ = stacked_local_ref_cookies_.back();
  stacked_local_ref_cookies_.pop_back();
}

可见:

  • PushFrame() 并没有新开辟内存:

    • 仅仅保存当前帧的“结束位置”,为后续 PopFrame() 提供“回滚点”;

    • 后续 add LocalReference 时再按需分配;

  • PopFrame() 也没有直接的内存释放逻辑:

    • 仅将局部引用表的逻辑顶部(top_index)回退到快照值;

    • 下次 delete LocalReference 才会真正触发 free list 和 hole 更新。

  • PushLocalFrame()/PopLocalFrame() 不是银弹:

    • 过分依赖可能造成内存释放不及时;

    • 忘记 pop 可能造成内存泄漏(可通过 RAII 封装);

class ScopedLocalFrame {
 public:
  ScopedLocalFrame(JNIEnv* env, int capacity = 16) : env_(env) {
    env_->PushLocalFrame(capacity);
  }
  ~ScopedLocalFrame() { env_->PopLocalFrame(nullptr); }
 private:
  JNIEnv* env_;
};

数组

  • GetStringUTFChars()GetIntArrayElements()GetByteArrayElements() 会返回 C 指针,需要通过 ReleaseStringUTFChars()ReleaseXXXArrayElements() 显式释放;

  • GetObjectArrayElement() 返回的 jobject (本地引用),无需手动释放 native 内存。

源码分析

static jobject GetObjectArrayElement(JNIEnv* env, jobjectArray java_array, jsize index) {
    //...
    return soa.AddLocalReference<jobject>(array->Get(index));
}

//////////

static jbyte* GetByteArrayElements(JNIEnv* env, jbyteArray array, jboolean* is_copy) {
    //...
    return GetPrimitiveArray<jbyteArray, jbyte*, ByteArray>(soa, array, is_copy);
}

static void ReleaseByteArrayElements(JNIEnv* env, jbyteArray array, jbyte* elements, jint mode) {
    ReleasePrimitiveArray<jbyteArray, jbyte, mirror::ByteArray>(env, array, elements, mode);
}

template <typename ArrayT, typename ElementT, typename ArtArrayT>
static ElementT* GetPrimitiveArray(JNIEnv* env, ArrayT java_array, jboolean* is_copy) {
    //...
    ObjPtr<ArtArrayT> array = DecodeAndCheckArrayType<ArrayT, ElementT, ArtArrayT>(
        soa, java_array, "GetArrayElements", "get");
    //...
    if (Runtime::Current()->GetHeap()->IsMovableObject(array)) {
      //...
      const size_t component_size = sizeof(ElementT);
      size_t size = array->GetLength() * component_size;
      void* data = new uint64_t[RoundUp(size, 8) / 8];
      memcpy(data, array->GetData(), size);
      return reinterpret_cast<ElementT*>(data);
    } else {
      //...
      return reinterpret_cast<ElementT*>(array->GetData());
    }
}

static void ReleasePrimitiveArray(ScopedObjectAccess& soa, ObjPtr<mirror::Array> array, size_t component_size, void* elements, jint mode) REQUIRES_SHARED(Locks::mutator_lock_) {
    void* array_data = array->GetRawData(component_size, 0);
    gc::Heap* heap = Runtime::Current()->GetHeap();
    bool is_copy = array_data != elements;
    size_t bytes = array->GetLength() * component_size;
    if (is_copy) {
      //...
      if (mode != JNI_ABORT) {
        memcpy(array_data, elements, bytes);
      } else if (kWarnJniAbort && memcmp(array_data, elements, bytes) != 0) {
        //...
      }
    }
    if (mode != JNI_COMMIT) {
      if (is_copy) {
        delete[] reinterpret_cast<uint64_t*>(elements);
      } else if (heap->IsMovableObject(array)) {
        //...
      }
    }
 }

可见:

  • 数值类的 JNI 数组 getter 会调用 GetPrimitiveArray(),内部可能发生 memcpy(),所以必须手动调用对应的 release 接口;

  • JNI object 数组 getter 直接返回 Local Reference,所以一般无需手动释放。

更进一步,它是通过 IsMovableObject() 判断是否需要拷贝的:

  • 首先判断是否使用可移动式 GC(ART 默认开启);

  • 然后判断当前内存区域是否属于可移动。

拷贝的深层原因是:某些 GC 算法为防止内存碎片,会主动将存活对象从一个内存区域(From-space) 移动到另一个区域(To-space),无法保证 Java 堆内存地址的不变性,所以必须拷贝。

// Garbage collector constants.
static constexpr bool kMovingCollector = true;
static constexpr bool kMarkCompactSupport = false && kMovingCollector;

bool Heap::IsMovableObject(ObjPtr<mirror::Object> obj) const {
  if (kMovingCollector) {
    space::Space* space = FindContinuousSpaceFromObject(obj.Ptr(), true);
    if (space != nullptr) {
      return space->CanMoveObjects();
    }
  }
  return false;
}

space::ContinuousSpace* Heap::FindContinuousSpaceFromObject(ObjPtr<mirror::Object> obj, bool fail_ok) const {
  space::ContinuousSpace* space = FindContinuousSpaceFromAddress(obj.Ptr());
  if (space != nullptr) {
    return space;
  }
  //...
  return nullptr;
}

space::ContinuousSpace* Heap::FindContinuousSpaceFromAddress(const mirror::Object* addr) const {
  for (const auto& space : continuous_spaces_) {
    if (space->Contains(addr)) {
      return space;
    }
  }
  return nullptr;
}

//////////

class Space {
 public:
  virtual bool CanMoveObjects() const = 0;
  //...
};

class RegionSpace final : public ContinuousMemMapAllocSpace {
 public:
  bool CanMoveObjects() const override {
    return true;
  }
//...
};

class LargeObjectSpace : public DiscontinuousSpace, public AllocSpace {
 public:
  bool CanMoveObjects() const override {
    return false;
  }
};

高效字节传输

DirectByteBuffer 可用于Java/JNI 之间高效共享内存,它属于堆外内存(off-heap memory),不存在隐式拷贝:

  • 如果是 Java 层分配的,GC 通过 Cleaner 释放;

  • 如果是 JNI 层分配的,需要手动 free() 释放。

Java 层创建

ByteBuffer buf = ByteBuffer.allocateDirect(1024);
nativeHandleBuffer(buffer);

JNI 层使用

NIEXPORT void JNICALL
Java_Foo_Bar_nativeHandleBuffer(JNIEnv *env, jobject obj, jobject byteBuffer) {
    // 获取 DirectByteBuffer 的起始地址
    void* address = (*env)->GetDirectBufferAddress(env, byteBuffer);
    if (address == NULL) {
        // 不是 DirectBuffer 或其他错误
        return;
    }

    // 获取容量
    jlong capacity = (*env)->GetDirectBufferCapacity(env, byteBuffer);

    // 直接读写内存
    memset(address, 0, capacity);
}

JNI 层创建并负责释放

NIEXPORT jobject JNICALL
Java_Foo_Bar_createBuffer(JNIEnv *env, jclass cls, jint size) {
    void* mem = malloc(size); // 或使用 mmap 等
    if (!mem) return NULL;

    // 创建 DirectByteBuffer 包装该内存
    jobject directBuffer = (*env)->NewDirectByteBuffer(env, mem, size);

    return directBuffer;
}

JNIEXPORT void JNICALL
Java_Foo_Bar_releaseBuffer(JNIEnv *env, jobject obj, jobject buffer) {
    void* addr = (*env)->GetDirectBufferAddress(env, buffer);
    free(addr); // 释放之前 malloc 的内存
}

源码分析

final Cleaner cleaner;
final MemoryRef memoryRef;

DirectByteBuffer(int capacity, MemoryRef memoryRef) {
    super(-1, 0, capacity, capacity, memoryRef.buffer, memoryRef.offset);
    // Only have references to java objects, no need for a cleaner since the GC will do all the work.
    this.memoryRef = memoryRef;
    this.address = memoryRef.allocatedAddress + memoryRef.offset;
    cleaner = null;
    this.isReadOnly = false;
}

////////////

final static class MemoryRef {
    byte[] buffer;
    long allocatedAddress;
    final int offset;
    boolean isAccessible;
    boolean isFreed;

    final Object originalBufferObject;

    MemoryRef(int capacity) {
        VMRuntime runtime = VMRuntime.getRuntime();
        buffer = (byte[]) runtime.newNonMovableArray(byte.class, capacity + 7);
        allocatedAddress = runtime.addressOf(buffer);
        offset = (int) (((allocatedAddress + 7) & ~(long) 7) - allocatedAddress);
        isAccessible = true;
        isFreed = false;
        originalBufferObject = null;
    }

    //...

    void free() {
        buffer = null;
        allocatedAddress = 0;
        isAccessible = false;
        isFreed = true;
    }
}

////////////

public class Cleaner extends PhantomReference<Object> {
    // Dummy reference queue
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    // Doubly-linked list of live cleaners, which prevents the cleaners
    // themselves from being GC'd before their referents
    static private Cleaner first = null;

    private Cleaner next = null, prev = null;

    private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {
            cl.next = first;
            first.prev = cl;
        }
        first = cl;
        return cl;
    }
    //...
}
template <bool kInstrumented = true, typename PreFenceVisitor>
  mirror::Object* AllocNonMovableObject(Thread* self, ObjPtr<mirror::Class> klass, size_t num_bytes, const PreFenceVisitor& pre_fence_visitor) {
    mirror::Object* obj = AllocObjectWithAllocator<kInstrumented>(self, klass, num_bytes, GetCurrentNonMovingAllocator(), pre_fence_visitor);
    //...
    return obj;
  }

AllocatorType GetCurrentNonMovingAllocator() const {
    return current_non_moving_allocator_;
}

template <bool kInstrumented, bool kCheckLargeObject, typename PreFenceVisitor>
inline mirror::Object* Heap::AllocObjectWithAllocator(Thread* self, ObjPtr<mirror::Class> klass, size_t byte_count, AllocatorType allocator, const PreFenceVisitor& pre_fence_visitor) {
    //...
    obj = TryToAllocate<kInstrumented, false>(self, allocator, byte_count, &bytes_allocated, &usable_size, &bytes_tl_bulk_allocated);
    //...
}

template <const bool kInstrumented, const bool kGrow>
inline mirror::Object* Heap::TryToAllocate(Thread* self, AllocatorType allocator_type, size_t alloc_size, size_t* bytes_allocated, size_t* usable_size, size_t* bytes_tl_bulk_allocated) {
    //...
    switch (allocator_type) {
        //...
        case kAllocatorTypeNonMoving: {
          ret = non_moving_space_->Alloc(self, alloc_size, bytes_allocated, usable_size, bytes_tl_bulk_allocated);
          break;
        }
        //...
    }
    //...
}

Heap::Heap(//...) {
    //...
    MemMap non_moving_space_mem_map;
    if (separate_non_moving_space) {
        //...
        if (heap_reservation.IsValid()) {
          non_moving_space_mem_map = heap_reservation.RemapAtEnd(heap_reservation.Begin(), space_name, PROT_READ | PROT_WRITE, &error_str);
    } else {
          non_moving_space_mem_map = MapAnonymousPreferredAddress(space_name, request_begin, non_moving_space_capacity, &error_str);
    }
    //...
    non_moving_space_ = space::DlMallocSpace::CreateFromMemMap(std::move(non_moving_space_mem_map),
                                                               "zygote / non moving space",
                                                               kDefaultStartingSize, initial_size, size, size,
                                                               /* can_move_objects= */ false);
    //...
}

可见 Android 版 DirectByteBuffer 实际采用了简化实现:

并未使用 Cleaner(PhantomReference), 而是通过 MemoryRef

  • 持有 Runtime 分配的不可移动内存的 address,既不会隐式拷贝、也不用自己释放,还能高效访问;

  • 维护可访问状态,读写操作都会检查;

字节序

Java 默认使用大端(IO 流、Buffer 等),ByteBuffer 可指定大小端:

ByteBuffer buf = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(0x12345678); // 写入小端: [0x78, 0x56, 0x34, 0x12]

此外,还有之前的几篇涉及 JNI 内存管理的文章:

OC/C++ 混编场景内存管理

OC/C++ 混编很爽,但内存管理时,他们是两个独立世界:
“上帝的归上帝,凯撒的归凯撒。”

自动释放池

  • OC 主线程默认有 AutoReleasePool,子线程才需要手动创建;

  • 但混编场景,C++ 函数最终被外部哪个线程调用无法预知,所以但凡内部调用 OC API 产生了 AutoRelease 对象 ,建议都包在 @autoreleasepool{} 代码块。

#define NSString2CharP(nsstr) [nsstr cStringUsingEncoding:NSUTF8StringEncoding]

std::string osVersion() {
    static const auto version = []() -> std::string {
        @autoreleasepool {
            NSString *nsVersion = UIDevice.currentDevice.systemVersion;
            if (nsVersion == nullptr) [[unlikely]] {
                return "Unknown";
            }
            const auto cstr = NSString2CharP(nsVersion);
            if (cstr == nullptr) [[unlikely]] {
                return "Unknown";
            }
            return {cstr};
        }
    }();
    return version;
}

弱引用

风险

  • OC 的弱引用机制:

    • dealloc 时,遍历 weak table,将所有指向该对象的 __weak 变量置为 nil

    • 前提是 __weak 变量必须位于 Runtime 可追踪的内存(OC 对象实例变量、全局/栈上变量);

  • C++ 对象的析构与 OC 对象的 dealloc 是两个独立生命周期系统:

    • C++ 成员变量通常不在 OC Runtime 的弱引用表监控范围内;

    • 编译器不会为 C++ 类中的 __weak 自动生成 objc_initWeak / objc_destroyWeak(实际上会退化为 __unsafe_unretained);

实际上,Clang 社区曾讨论过是否应禁止在 C++ 成员中使用 __weak,但为了兼容旧代码,最终选择“允许但不实现语义”。

所以,如果 C++ 对象直接持有 OC __weak 成员,会导致严重内存问题:

  • __weak 不会被自动置 nil,成为悬垂指针;

  • 后续消息发送 EXC_BAD_ACCESS 直接 crash。

class BadCppClass {
    __weak id<FooProtocol> weakDelegate; // C++ 成员,OC Runtime 无法追踪
public:
    BadCppClass(id<FooProtocol> mDelegate) : weakDelegate(mDelegate) {}
    
    void doSomething() {
        [weakDelegate process]; // 一旦外部释放,成为悬垂指针,可能 crash
    }
};

解决方案

  • 单独定义 NSObject 子类持有 __weak id

  • C++ 构造函数利用 id 创建 OC 对象,析构函数显式释放 OC 对象(MRC 调用 release,ARC 置为 nil)。

@interface WeakRef : NSObject
@property (nonatomic, weak) id<FooProtocol> weakDelegate;
@end
@implementation WeakRef
@end

class GoodCppClass {
    WeakRef* _ref;
public:
    GoodCppClass(id<FooProtocol> mDelegate) {
        _ref = [[WeakRef alloc] init];
        _ref.weakDelegate = mDelegate; // Runtime 知道这个 weak 引用
    }
    ~GoodCppClass() {
        _ref = nil;
    }
    void doSomething() {
        [_ref.weakDelegate process];
    }
};

可通过 -Wunsupported-weak-receiver-type-Wobjc-weak-cxx-member 开启编译警告;

字节序

ObjC 为了兼容 C,默认也是小端。
Core Foundation 通过 CFByteOrder.h 提供大小端相关 API:

//获取当前机器大小端模式:
CFByteOrderGetCurrent(); 

//将 32 位的整型从大端转为本机的模式(若本机为大端,则原值不变)
uint32_t CFSwapInt32BigToHost(uint32_t arg);

//将 32 位的整型从本机的模式转为大端(若本机为大端,则原值不变)
uint32_t CFSwapInt32HostToBig(uint32_t arg);

//将 32 位的整型从小端转为本机的模式(若本机为小端,则原值不变)
uint32_t CFSwapInt32LittleToHost(uint32_t arg);

//将 32 位的整型从本机的模式转为小端(若本机为小端,则原值不变)
uint32_t CFSwapInt32HostToLittle(uint32_t arg);

此外,还有之前的一篇关于 OC/C++ 混编的文章有涉及内存管理相关问题:

iOS 引用 C/C++ 项目:交叉编译与 Objective-C++