简介:java2op for xe10是一套面向Embarcadero Delphi开发人员的Java互操作转换工具,可将Java jar包解析并生成Delphi可引用的PAS单元文件,解决在XE10.2环境中复用Java类库的难题。资源共8个文件,约16.4MB,包含主程序Java2OP.exe、两个jar库文件、inc/xml/txt配置模板以及log日志,工具链从解析、转换到生成均有对应组件,适合需要连接Java生态的Delphi项目。已有258人学习下载。借助Java2OP.exe可直接解析JAR内类、接口、方法等元数据,并转换为Delphi语法结构输出为PAS单元,同时支持通过模板调整生成结果;包内辅助Jar与日志文件可帮助排查转换异常、比对映射差异,为手工修正类型或调用约定提供依据。针对XE10.2优化的生成规则,使PAS单元能较好匹配该版本编译器特性,降低集成成本,进一步保障跨语言调用的稳定性。对于希望重用现有Java库、减少重复编码并拓展跨平台能力的Delphi开发者,这份工具包能带来直接的效率提升与落地价值。
1. 把 jar 变成 Delphi 能调用的 pas:java2op 解决了 Android 开发里最烦的一步
在 Delphi XE10 做 Android 开发的人应该都有过这种经历:项目需要调用某个 Java SDK 的接口,但 Delphi 不认 jar 包里的类,你只能在 Java 那边写一个中转壳子再通过 JNI 手动调用,或者干脆放弃原生集成改用 Web 接口。java2op 这个工具的作用,就是把你手里的 jar 包或 class 文件直接转成 Delphi 能识别的 pas 源文件,让 Java 类像 Delphi 自己的类一样被 new、调方法、读属性。它解决的不只是转换,而是整个 Android + JNI 桥接链路的打通——从字节码解析到数据类型映射再到 JVM 调用代码生成。适合那些已经能在 Delphi XE10 里跑通基础 Android 工程、但被 Java 依赖卡住的人。
2. JNI 桥接的原理:java2op 到底在替你做什么
2.1 为什么 Delphi 不能直接调用 Java 类
Android 系统里的 Java 层运行在 ART 虚拟机里,Delphi 编译出的原生代码跑在 Native 层,两者之间隔着一道 JNI 边界。JNI 本身是一套 C 风格的函数接口,Delphi 可以通过 System.JNI 单元直接调用它,但问题在于:你要调用的不是一个 JNI 函数,而是上百个 Java 类方法,每个方法都要手写 jclass、jmethodID 的获取逻辑,还要处理字符串转换、对象引用管理、异常检查,工作量非常大。
java2op(配合 jar2pas、class2pas)做的事情,就是把 Java 字节码里的类结构、方法签名、字段签名、继承关系全部解析出来,生成对应的 Delphi 接口定义和实现代码。生成的 pas 文件里,每个 Java 类对应一个 Delphi 接口和实现类,方法被包装成 Delphi 函数,内部自动完成 jclass 缓存、jmethodID 查找和参数转换。
从运行机制上说,工具生成的代码和你手写的 JNI 调用底层是同一套机制,但它把重复劳动自动化了。你可以把它理解为代码生成器,不是运行时框架——它不替你执行 Java 代码,只是铺好管道。
2.2 三种转换模式的适用边界
这类工具链通常支持三种输入格式:jar 包(最常用)、编译后的 class 文件、以及直接的 Java 源码文件。选择依据有两个:你的依赖是完整 SDK 还是单文件、以及有没有混淆(ProGuard 处理过)。
jar 包模式适合大多数场景。你从厂商那里拿到一个 aar 或 jar,里面通常是编译好的字节码,jar2pas 会把整个包内所有类批量转换。class 文件模式适用于你本机编译出的单个 Java 文件的场景,比如你自己写了一个辅助类用来包装第三方 SDK,那把 .class 转过去就够了。源码模式最省事,工具会用内部编译器把 .java 编译成字节码再转换,但前提是你本机安装了 JDK 并且环境变量配好了。
参数上,我一般习惯先在源码模式跑通一个小文件,确认 JDK 路径和环境变量没问题,再切换到 jar 模式做批量转换。直接上来就转大 jar 包,报错了会让你分不清是工具问题还是 JDK 问题。
2.3 jni 签名映射表:转换结果正确与否的关键
转换质量的核心在 JNI 签名的映射处理上。工具在解析 Java 字节码时,需要把 Java 类型表到 JNI 签名,再对应到 Delphi 类型。这个表是理解后续所有问题的钥匙:
| Java 类型 | JNI 签名 | Delphi 类型 | 说明 |
|---|---|---|---|
| boolean | Z | Boolean | 1 字节 |
| byte | B | ShortInt | 有符号 8 位 |
| char | C | Char | UTF-16 |
| short | S | SmallInt | 有符号 16 位 |
| int | I | Integer | 32 位 |
| long | J | Int64 | 64 位,Delphi 里是原生整数 |
| float | F | Single | 32 位浮点 |
| double | D | Double | 64 位浮点 |
| String | Ljava/lang/String; | JString | 需通过 JNI 转换接口 |
| 数组 | [类型签名 | TJavaArray | 见说明 |
字符串和数组是转换中的重灾区。Java 的 String 对应 JString,底层是一个 jstring 句柄,Delphi 侧不能直接当普通字符串用,必须调用 GetStringUTFChars 取出来再转成 Delphi string。工具生成的代码里会做这层包装,但如果你自己补代码,很容易漏掉释放那一步,导致内存只在垃圾回收时释放。
数组类型生成的代码默认是引用传递,这在大多数调用场景下没问题。但如果你要从 Java 侧获取一个 byte[] 出来做文件写入,建议先查一下生成代码里有没有提供元素访问的辅助方法,没有的话自己用 TJNIResolver 包装。
3. 环境准备与转换流程:从 jar 到 pas 的三个操作步骤
3.1 工具链安装与 JDK 环境校验
先确保环境整齐。java2op 这类工具依赖 JDK 来解析字节码,所以 JDK 要先装好。安装时注意:Delphi XE10 的 Android 开发用的是 Android SDK,带的是 Android 版本的 JDK 类库,但工具解析 jar 包时用的是你机器上的 JDK 工具类,版本不一致会报 class format 错误。
我常用的环境组合是 JDK 8 + Delphi XE10 某个较新更新版。JDK 8 能解析绝大多数 Android 字节码(Android 的 class 版本通常控制在 50 及以下),JDK 11 也能跑但遇到老项目容易提示 version 55 错误。装好后打开命令行验证一下:
java -version javap -version注意,如果你看到 java -version 正常但 javap 提示找不到命令,说明 JRE 在 PATH 里而 JDK 没在,程序编译类文件时会失败。这种情况把 JDK 的 bin 目录加到系统 PATH 前面即可,但加完之后重启一下命令行窗口才生效。
Delphi 侧也确认一下你有 System.JNI 单元。打开 Delphi 后新建一个空 Android 工程,在 uses 里手动加一下 System.JNI,编译能过就没问题。它存在于 FMX 和标准库中,不存在就说明你的 XE10 版本太老,要先更新。
3.2 用 java2op 转换:命令参数与四处必调配置
工具本身是命令行程序,也有带界面的版本。命令行方式更可控,我在脚本化处理多个 jar 包的时候都用命令行。基本用法如下:
java2op.exe -jar yourlib.jar -o output_dir -d DelphiVersion -c参数含义分几个层级:-jar 指定输入 jar 包路径,-o 指定输出目录,-d 指定 Delphi 版本(XE10 对应填 10),-c 表示同时生成实现文件(不填则只生成接口)。这些参数里有两个坑值得说明。
第一个坑是包名过滤。默认转换 jar 包里的所有类,但你的 jar 可能带了第三方依赖,比如某些 SDK 内置了 okhttp 或 gson 的代码。这种全量转换会让生成文件膨胀且容易出现符号冲突。我一般在 -jar 后面跟一个 -p 参数限定包前缀:
java2op.exe -jar yourlib.jar -o output_dir -d 10 -c -p com.example.sdk只转换 com.example.sdk 前缀下的类,第三方依赖留给运行时由 jar 包本身加载。这样生成的 pas 文件会精简很多。
第二个坑是输出文件的编码。老版本工具默认输出 UTF-8,而 Delphi 的源码默认是 ANSI。如果你的 jar 包里的类名或注释包含非 ASCII 字符,生成的 pas 文件在 Delphi 里打开会乱码,甚至编译报错。解决办法是让工具输出 UTF-8 后,再用编辑器批量转成 UTF-8 with BOM,Delphi 对带 BOM 的 UTF-8 识别最稳定。文件多的时候我用一个小脚本批量处理,不手工一个个转。
命令行工具会在结束时打印生成的文件列表和警告信息。务必逐条看警告。最常见的警告是某个类引用了 jar 包之外的类,工具会在生成的 pas 里标记为 external 引用,意思是运行时从类路径里找。如果你确定这个类在你的 jar 包里,那就是包前缀过滤太严格了,放宽 -p 参数重新转一次。
3.3 转换结果的结构检查
拿到生成的 pas 文件后,先别急着复制进 Delphi 工程。我习惯先打开文件做三项检查,每项都省时间。
第一,看接口声明是否完整。Java 类对应 Delphi 接口,接口里每个方法对应一个函数声明,方法名和参数类型要和 Java 侧的 public 方法保持一致。如果有方法缺失,检查是不是 Java 里方法被标记成了 protected 或包私有,工具默认只转 public 方法。
第二,看类引用是否需要用 import 单元。生成文件开头会 contains 或者 uses 依赖单元,比如 java.lang.System、java.util.ArrayList 这些基础类,在 Delphi 里已经通过 System.JNI 和 AndroidAPI 覆盖,不需要额外导入 jar 包。但如果生成的 uses 列表里有你从没见过的单元,那说明工具尝试引用了某些辅助文件,你要确认它们是否也生成在了同一目录里。
第三,看有没有生成独立的初始化和释放过程。接口和实现之外,工具通常会在末尾生成两个方法:JClassInit 和 JClassFinalize(或类似名字),分别在应用启动时和退出时调用。它们的职责是缓存 jclass 引用和释放全局引用。很多人忽略这两个函数,导致第二次调用时 Java 类找不到或者引用泄漏。后面集成的时候必须调用它们。
3.4 在 Delphi 工程里接入生成的 pas
文件放进工程后,uses 里加上对应单元,然后在 Application 启动事件里先调用初始化。初始化那块我会写成这样:
uses System.SysUtils, System.JNI, uGeneratedSDK; procedure TMainForm.FormCreate(Sender: TObject); var JVM: TJNIResolver; LClass: JClass; begin JVM := TJNIResolver.GetJNIEnv; // 初始化生成的类引用 TJ_YourClass.JClassInit(JVM); // 验证类是否可以加载 LClass := TJ_YourClass.JClass; if LClass = nil then raise Exception.Create('Java class load failed - check jar in classpath'); end;关键点在于 TJNIResolver 的获取时机。它必须在 Java 虚拟机已启动之后才能正常工作。Delphi Android 应用启动时 JVM 已经由系统创建好了,FMX 框架会处理好这一点,你只需要确认这行代码不是在后台线程执行的。JClassInit 方法会通过 JNIEnv 去查找 jclass 并缓存起来,这个操作本身是重量级的,只应该在启动阶段做一次,不要在每次调用时重复执行。
初始化成功之后再调用具体方法:
var LJavaStr: JString; LResult: Integer; begin LJavaStr := StringToJString('test input'); LResult := TJ_YourClass.JMethod(LJavaStr); JStringToString(LJavaStr); // 回收临时引用 end;注意最下面的 JStringToString 调用,这行不是必须的,但 StringToJString 生成的局部引用会在当前 JNI 帧结束后自动释放,不必显式回收。如果你在循环里大量构造 JString,建议在循环末尾手动调用一次 DeleteLocalRef,避免局部引用表溢出——这个坑后面还会细说。
4. 项目集成实战:以模拟项目X为示例完整走一遍
4.1 准备一个带第三方依赖的 jar 包
为说清楚整个过程的边界问题,我拿一个模拟项目X来演示:这个项目里有一个 Java SDK 的 jar 包,它依赖了通用 JSON 库(类似 org.json 那种)。jar 包里既有 SDK 自己的类,也有被打进去的第三方包。很多人第一反应是用默认参数直接整体转,结果生成的 pas 文件巨大,编译还报重复定义。
我先用压缩软件打开 jar 包看一下目录结构,确认第三方包的包名前缀。然后用上面说的 -p 参数限定 SDK 自己的包前缀,把第三方依赖排除在外。这么做的原因很简单:第三方 JSON 库在 Android 系统里往往已经有同版本实现,运行时直接由系统提供,Delphi 侧不需要也不应该引用它。
转换完成之后,我在生成的 pas 文件里搜一下有没有 JSONObject 相关的引用。正常情况应该找不到,因为在生成 Delphi 代码里这些类型没有对应定义。如果你在调用时需要把 Java 的 JSONObject 当作参数传进去,就得在 Delphi 侧用 JObject 泛型接口来承接,运行时它真正指向的对象是 Java 侧创建的。
4.2 封装调用层:把生成的接口包一层自己的类
直接生成的 pas 文件可以放进工程调用,但接口风格是扁平化的,每个 Java 类对应一个接口、一个实现类,读起来不直观。我更推荐再包一层 Delphi 类,把业务相关的调用收敛起来。
举个例子,SDK 里有一个 OrderService 类,里面有 createOrder 和 queryOrder 两个方法。生成的接口里就是两个函数,我封装成:
type TOrderServiceWrapper = class private FOrderService: JOrderService; public constructor Create; function CreateOrder(AUserId: string): Integer; function QueryOrder(AOrderId: Integer): string; end;构造函数里做初始化工作,业务方法里做类型转换和状态检查。这样的好处是 UI 层不用关心 JString 和 String 的转换,也把 JClassInit 的调用时机收敛到了一个对象里管理。
注意封装时不要丢掉异常信息。Java 方法抛出的异常在 Delphi 侧表现为 JNI 函数返回异常状态,但不会直接变成 Delphi 异常。工具生成的代码通常不做异常检测,你需要自己检查异常状态并抛出一个 Delphi 异常来承接。我在封装类里写了一个异常检查过程,每个方法调用后统一执行一遍:
procedure TOrderServiceWrapper.CheckJNIException; var LEnv: PJNIEnv; LThrowable: JThrowable; LMsg: JString; LMsgStr: string; begin LEnv := TJNIResolver.GetJNIEnv; if LEnv.CheckException(LEnv) then begin LThrowable := LEnv.ExceptionOccurred(LEnv); LEnv.ExceptionClear(LEnv); LMsg := LThrowable.toString(LThrowable); LMsgStr := JStringToString(LMsg); raise Exception.CreateFmt('Java exception: %s', [LMsgStr]); end; end;这段代码的主要作用是调用 ExceptionClear 把 JNI 异常状态清除掉,否则下一次 JNI 调用会直接失败而且不报错。用 toString 拿到的消息能辅助定位问题,比看 logcat 更快。
4.3 调用时机与生命周期边界
Delphi Android 应用里,JNI 调用有几个时间窗口是安全的:主界面的初始化事件里、用户事件响应里、定时器回调里。有几个位置千万不能调用——Application 的 OnCreate 之前(此时 Java 侧可能还没完成初始化)、OnPause 之后(有些 Java 对象状态可能已被系统回收)、以及后台线程直接调 UI 相关 Java 方法(不一定崩,但偶发异常会让你排查到怀疑人生)。
另外,系统会在某些时刻触发 Java 侧的垃圾回收。工具生成的代码里缓存了 jclass 全局引用,这个引用不受垃圾回收影响,但方法返回的局部引用(jobject、jstring)会在当前 JNI 帧结束时被释放。如果你在循环里拿到了一个对象引用并保存下来,下次循环再取时会发现它已经失效。
针对这个生命周期问题,我用一个工具方法处理所有从 Java 侧返回的对象引用。它的作用是复制一个全局引用并保存到 Delphi 对象里,方法返回前用 DeleteLocalRef 清掉局部引用。这个写法虽然会让代码看起来啰嗦,但能保证在 Java 侧发起垃圾回收后,Delphi 侧拿到的引用始终有效:
function RetainGlobal(LEnv: PJNIEnv; const LObj: JObject): JObject; begin Result := LEnv.NewGlobalRef(LEnv, LObj); LEnv.DeleteLocalRef(LEnv, LObj); end;NewGlobalRef 创建的引用必须成对释放,用完后调用 DeleteGlobalRef,否则会泄漏 Java 堆内存。Delphi 这边封装类最好在析构函数里统一释放所有保存的全局引用。
4.4 接口回调场景:Java 侧调回 Delphi 代码
调 SDK 时最常见的需求是异步回调。Java SDK 接收一个 listener,事件发生时调回来。这个场景在 JNI 桥接里属于高难度工作:工具不会帮你生成 Delphi 实现类给 Java 侧调用。
做法是写一个 Delphi 类继承 JObject 并实现 Java 侧的接口,然后通过 RegisterNatives 或直接调用 Java 的接口实现类来传递。我一般这样处理:先用工具生成接口类的 pas 定义,然后手写一个 Delphi 类实现这个接口。关键点是类的继承关系,Delphi 里实现 Java 接口的类必须继承自 TJavaGenericImport 或直接实现 JNI 层的回调函数。
这个环节是整套方案里最容易翻车的地方,有两个前置条件必须满足:一是 Delphi 类必须重写 GetJNIClass 方法返回正确的 JNI 类名,二是方法注册要在主线程完成。如果 SDK 初始化在后台线程,回调注册事件又发生在不同线程,则必须用 Delphi 的 TThread.Synchronize 把回调事件切回主线程,否则后续 UI 更新会触发异常。整体上我建议回调越简单越好——收到回调后立刻将相关数据拷贝成 Delphi 侧的基本类型,不在回调里做耗时操作。
5. 避坑与排查:五个常见问题及处理记录
5.1 生成的 pas 文件编译报找不到 JClassInit 方法
现象:编译 Delphi 工程时报错,提示找不到 JClassInit 或类似初始化函数,只报了一个或几个类有这问题。
原因:转换时生成策略可能只生成了接口文件,实现文件没生成,或者 -c 参数没带到命令行里。另一种可能是这些类的 Java 定义里只有静态方法,没有实例方法,工具跳过了实现代码生成。
解决:重新执行转换并确认 -c 参数带上了。对有静态方法的类,检查生成文件的尾部是否生成了静态方法的实现块。如果确实没有生成但调用时需要,就手动在 Pascal 里用 TJNIResolver 缓存 jclass 并写一个初始化过程,只维护你需要的静态方法。
5.2 JString 转换后中文变成问号或乱码
现象:从 Java 侧拿到的字符串在 Delphi 里显示乱码,特别是中文内容变成问号。
原因:JNI 标准的 GetStringUTFChars 输出的是 Modified UTF-8,Delphi 的 JStringToString 在早期 XE10 版本里对代理对的转换不完整。遇到 emoji 或特殊字符时会丢字节。
解决:换用 JNI 提供的 GetStringChars 获取 UTF-16 内容再手动转 Delphi string。在工具生成的代码基础上,自己写一个转换函数替代默认的 JStringToString,核心是取 UTF-16LE 字节数组后交给 Delphi 的 String 解码。一个额外的建议:在 Java 侧不要用四字节 emoji 作为业务数据,不是转换不了,而是后端的处理链路里很多环节都会出问题。
5.3 相同 jar 包在某些设备上崩、在另一些设备上正常
现象:同一个 APK 在同一品牌不同型号的设备上表现不一致,一部分正常,另一部分首次调用就崩溃或报 ClassNotFound。
原因:厂商 ROM 对 Java 类库的裁剪。某些设备商将 Android 系统里部分不常用的 SDK 类移除了,你的 jar 包编译引用到这些类时,在特定设备上执行到对应代码段才崩溃。这种崩溃和调用代码本身无关,是系统差异问题。
解决:先用一个启动时自检的 Java 类把所有关键类逐一加载一遍,得到一份兼容性清单;对缺失的类再决定是换用系统 API 还是降级功能。我一般会在 Java 侧写一个静态方法批量校验后返回布尔数组,Delphi 启动时读取并记录日志,这样用户报错时能看到明确原因而非崩溃堆栈。
5.4 循环调用 JNI 后程序内存持续上涨
现象:程序长时间运行或频繁刷新时,内存占用稳步增加,最终被系统回收或卡顿。
原因:大量局部引用未及时释放。JNI 局部引用在方法返回时会释放,但如果你在 Delphi 里保存了对象引用或者在循环里调用 Java 方法,局部引用表可能已经满了,JNI 出于保护会拒绝分配新引用,调用直接失败或崩溃。
解决:循环场景里增加局部引用表清理。可以在循环每完成一定次数后调用 EnsureLocalCapacity 检查容量,也可以把循环体拆到一个单独 Delphi 过程里,让局部引用随栈帧释放。代码层面数据获取完就立刻删除局部引用,不要保留 Java 对象的引用超过一次调用周期,除非用全局引用确保持久。
5.5 同一个 jar 包在 Debug 模式正常、Release 模式崩溃
现象:调试运行没问题,打包发布后运行到某个 Java 调用点就崩。
原因:Release 模式下 Delphi 的连接器会剔除编译单元里未引用的代码,而你生成 pas 里的初始化过程可能没有被显式调用,导致相关全局变量的初始化代码被优化掉了。JNI 相关代码如果没在主模块中被引用,会有类似问题。
解决:在工程的某个 AlwaysLink 单元或主单元里显式引用所有生成类的初始化过程。我习惯在工程启动时逐个调用生成类的 JClassInit 方法,并保留调用结果在同一源代码里,防止链接器认定该代码不可达。同时开启保持引用的链接器选项,把这一项固定在工程配置里,别依赖全局默认值。
6. 进阶用法:对生成的 pas 文件做三处手工裁剪,让代码更稳更快
6.1 第一处:把约定俗成的常量收敛到一个配置单元
生成的文件里会有大量 Java 常量值、字符串标签、方法名签名。我通常把它们系统地移到一个统一单元里统一管理。常量集中管理带来的直接好处是:后续升级 jar 包时只需要重新生成库文件,而不需要理会常量单元的内容,除非 Java 侧确实改了常量值。另一个好处是,代码审查时搜索常量定义只查一个文件,对照 Java SDK 的字段表非常快。
unit uSDKConstants; interface const SDK_STATE_IDLE = 0; SDK_STATE_RUNNING = 1; SDK_STATE_ERROR = -1; METHOD_SIGNATURE_CREATE_ORDER = '(Ljava/lang/String;I)I'; implementation end.把签名常量单独声明还有一个实际作用:如果工具生成的方法调用代码出错,你可以直接用这些签名做手动 JNI 调用,不用重新解析 jar。
6.2 第二处:把使用频率高的字符串转换做成内联函数
生成代码里 StringToJString 和 JStringToString 调用点非常多。每次转换都至少涉及一次局部引用创建和一次释放。这类纯转换逻辑适合做成内联函数,减少调用开销。
function JStr(const S: string): JString; inline; begin Result := StringToJString(S); // 临时局部引用,随 JNI 帧自动释放 end;注意内联函数只适合无状态、无资源持有场景。上面这个 JStr 函数返回的 JString 是局部引用,如果调用方把它保存下来超过当前 JNI 帧,引用会失效。这种情况下签字要改成全局引用版本,并在调用方用完显式释放。
6.3 第三处:给所有 Java 方法调用加一个超时保护
JNI 调用本身没有超时概念,Python 那种 Hanging 的情况在 JNI 里同样存在——如果 Java 侧的方法因为底层网络或锁竞争长期不返回,Delphi 主线程会被卡死。在调用很可能涉及网络、IO 或锁的方法时,我会把调用放到后台线程执行,同时在 Delphi 侧用 TThread 加一个超时判断。Java 方法本身通常不响应中断,所以这个超时只能防止 UI 线程卡死,不能真正终止 Java 侧执行。
function CallWithTimeout(const AProc: TFunc<Integer>): Boolean; var LThread: TThread; LResult: Integer; begin Result := False; LThread := TThread.CreateAnonymousThread( procedure begin LResult := AProc(); TThread.Synchronize(nil, procedure begin Result := True; end); end); LThread.FreeOnTerminate := False; LThread.Start; LThread.WaitFor(3000); // 等待最多 3 秒 LThread.Free; end;这个写法属于兜底手段,正常情况不会触发超时分支。一旦触发了,更重要的是排查为什么 Java 方法花了超过预期的时间,而不是在超时分支里做更多补救。
这三处裁剪做完,我拿到新的 jar 包时的处理流程就是固定的了:先整体转换、删除不必要依赖、抽出常量、封装调用层、自检异常、跑一遍循环场景看内存曲线,全部通过后才接进主工程。做过的项目多了以后,我发现大部分疑难问题都出现在引用生命周期管理和线程切换上,工具生成代码本身基本是稳定的。JNI 这层没有玄学可讲,出现问题就是引用、线程、签名三者之一,按照先查引用泄漏、再查线程切换、最后核对签名的顺序排查,没有解决不了的问题。希望这些经验能帮你少走几步弯路。
本文还有配套的精品资源,点击获取