Android JNI开发C++库时std::unordered_map/set偶发崩溃求助
嘿,我来帮你捋这个让人头疼的偶发崩溃问题——只在部分设备出现、自己复现不了,这种场景下线程安全问题是头号嫌疑人,其次是自定义类型的STL适配或者NDK环境的差异,咱们一步步拆解:
1. 线程安全:最可能的元凶
std::unordered_map和std::unordered_set本身完全不是线程安全的:
- 如果你的黑名单添加操作(比如Java层多线程调用JNI方法往
mData里塞数据)和校验操作(比如另一个线程查黑名单)同时跑,容器的内部结构很容易被打坏——比如迭代器失效、哈希表扩容时的并发冲突,直接触发崩溃。 - 为啥只在部分设备出问题?因为不同设备的CPU核心数、线程调度策略不一样,高并发的设备更容易撞上冲突场景,纯粹是概率问题。
怎么修?
给mData的所有读写操作加锁就行,用C++11的std::mutex最方便,确保同一时间只有一个线程能碰容器:
#include <mutex> // 你的黑名单容器 std::unordered_set<CodePoints> mData; // 对应的锁 std::mutex mDataLock; // 添加词汇的函数 void addToBlacklist(const CodePoints& newItem) { // lock_guard会自动解锁,不用手动管 std::lock_guard<std::mutex> lock(mDataLock); mData.insert(newItem); } // 校验是否在黑名单的函数 bool isBlacklisted(const CodePoints& checkItem) { std::lock_guard<std::mutex> lock(mDataLock); return mData.find(checkItem) != mData.end(); }
要是Java层多线程调用JNI方法,一定要记住:JNI方法本身不会自动加锁,所有涉及mData的操作都得裹在锁里。
2. CodePoints的STL适配问题
你给的CodePoints构造函数片段能看到是用insert初始化内部的mCodePoints,但作为std::unordered_set的元素,这个结构体得满足两个关键要求,不然很容易出问题:
- 得有自定义哈希函数:
std::unordered_set需要知道怎么计算CodePoints的哈希值,没这个的话,插入、查找都会乱套,甚至触发未定义行为。 - 得有相等判断运算符:容器要能判断两个
CodePoints是不是同一个元素,不然会出现重复插入或者查不到的情况,极端情况下也会崩溃。
另外,构造函数里如果codePointsCount和实际传入的数组长度不匹配,insert时会踩非法内存,这种偶发越界也会导致崩溃。
怎么修?
补全哈希和相等判断,再加个参数校验:
// 自定义CodePoints的哈希函数 namespace std { template<> struct hash<CodePoints> { size_t operator()(const CodePoints& cp) const { size_t hashVal = 0; // 把所有code point的哈希值组合起来,避免冲突 for (int c : cp.mCodePoints) { hashVal ^= std::hash<int>()(c) + 0x9e3779b9 + (hashVal << 6) + (hashVal >> 2); } return hashVal; } }; } // 相等判断运算符 bool operator==(const CodePoints& lhs, const CodePoints& rhs) { return lhs.mCodePoints == rhs.mCodePoints; } // 补全你的CodePoints构造函数,加参数校验 struct CodePoints { std::vector<int> mCodePoints; explicit CodePoints() { } explicit CodePoints(const int * codePoints, const int codePointsCount) { // 先做参数校验,避免空指针或非法长度 if (codePoints == nullptr || codePointsCount <= 0) { return; } mCodePoints.insert(mCodePoints.end(), codePoints, codePoints + codePointsCount); } };
3. NDK的STL实现差异
不同Android设备可能用不同的STL版本(比如旧的gnustl或者新的c++_shared),某些旧STL的std::unordered_map/set实现有bug,比如并发场景下的内存破坏,或者对自定义类型的支持有问题。
怎么修?
- 升级到最新的NDK版本,尽量用官方推荐的
c++_shared或c++_static(推荐c++_shared,体积小还修复了不少旧bug)。 - 在CMake或者Application.mk里明确指定STL版本,比如CMakeLists.txt里加:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") # 用shared版STL set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -shared-libstdc++")
4. JNI层的内存引用坑
如果Java层传入的codePoints数组,你在JNI层没拷贝数据,反而直接存了指针,那当Java GC回收这个数组后,指针就变成野指针,访问时就会崩溃——这种情况也是偶发的,取决于GC的时机。
怎么修?
你现在用insert拷贝数据是对的,一定要保持这个逻辑,别图省事存指针,确保CodePoints内部持有自己的拷贝数据。
最后,建议你去Fabric看崩溃的栈回溯——如果栈里显示是在std::unordered_set::insert或者哈希计算的时候崩的,那线程安全或STL适配的问题概率最高;要是在内存访问的时候崩,那大概率是越界或者野指针。
内容的提问来源于stack exchange,提问作者huy nguyen

