Android如何通过JNI获取Java Canvas底层SkCanvas指针实现Skia C++绘制
背景
在Android平台开展基于Java Canvas API的原生绘制实验时,通常会将View OnDraw()回调中获取的Canvas对象通过JNI传入Native层,调用C++侧Skia API完成绘制。核心待解决问题为:Java层Canvas对象传入Native后,如何正确获取对应的底层SkCanvas指针。
已知源码链路与卡点
- Java层Canvas的私有成员
mNativeCanvasWrapper定义在BaseCanvas类中,可通过反射调用被@UnsupportedAppUsage标记的隐藏方法getNativeCanvasWrapper()获取该long类型的Native句柄,对应方法实现:
/** @hide */ @UnsupportedAppUsage public long getNativeCanvasWrapper() { return mNativeCanvasWrapper; }
- 上述long型句柄会作为参数传入所有Canvas相关的Native绘制方法,跟踪
getWidth()调用链可在JNI实现中看到对应注册逻辑:
{"nGetWidth","(J)I", (void*) CanvasJNI::getWidth},
- JNI层内置的
get_canvas工具方法会直接将传入的long型句柄强转为Canvas*基类指针,对应实现:
static jint getWidth(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle) { return static_cast<jint>(get_canvas(canvasHandle)->width()); } static Canvas* get_canvas(jlong canvasHandle) { return reinterpret_cast<Canvas*>(canvasHandle); }
- 该
Canvas*指针实际指向继承自Canvas基类的SkiaCanvas实例,其内部持有目标SkCanvas指针,但该成员为私有属性,唯一的getter方法为protected权限,无法直接从外部访问。
可落地的获取方案
方案1:继承访问Hack(利用C++访问控制规则)
由于SkiaCanvas获取内部SkCanvas的方法为protected权限,可在Native代码中定义一个继承自SkiaCanvas的辅助访问类,直接调用基类的protected方法拿到指针,示例代码:
// 需提前引入对应版本的SkiaCanvas头文件,注意匹配目标系统的类定义 class SkiaCanvasBridge : public SkiaCanvas { public: static SkCanvas* extract(SkiaCanvas* canvasInst) { // 注意不同Android版本方法名有差异:部分版本为asSkCanvas(),部分为getCanvas(),对照头文件调整即可 return canvasInst->asSkCanvas(); } };
使用时直接传入强转后的SkiaCanvas指针即可:
SkiaCanvas* skiaCanvas = reinterpret_cast<SkiaCanvas*>(canvasHandle); SkCanvas* targetCanvas = SkiaCanvasBridge::extract(skiaCanvas);
注意:该方案需要和目标运行设备的Android版本AOSP头文件匹配,若类方法名、继承关系发生变更需要同步调整。从Android 8.0引入Skia作为HWUI后端至今,所有正式版本的Java层Canvas对应的Native实例均为SkiaCanvas派生类,不存在其他实现,类型强转不会出错。
方案2:内存偏移取值(无依赖头文件场景适用)
C++非多态类的成员内存布局在同编译产物下偏移固定,可对照目标版本的SkiaCanvas.h头文件,计算SkCanvas*成员相对于类首地址的偏移量,直接通过地址运算取值,示例:
SkiaCanvas* skiaCanvas = reinterpret_cast<SkiaCanvas*>(canvasHandle); // 偏移值必须对照目标版本源码计算,以下32为示例值,不可直接使用 uintptr_t skCanvasMemberAddr = reinterpret_cast<uintptr_t>(skiaCanvas) + 32; SkCanvas* targetCanvas = *reinterpret_cast<SkCanvas**>(skCanvasMemberAddr);
注意:不同Android版本的SkiaCanvas成员顺序会随功能迭代调整,偏移值必须匹配目标系统源码,否则会触发野指针崩溃。
方案3:公开NDK API(Android 12+ 无Hack方案)
从Android 12(API 31)开始,NDK正式暴露了Canvas与Skia对接的公开接口,可直接调用ACanvas_getSkCanvas方法传入之前获取的native canvas句柄,直接拿到合法的SkCanvas指针,不需要任何Hack操作,适合仅面向高版本系统的场景。
注意事项
- 所有方式获取的SkCanvas指针仅在当前
OnDraw()回调生命周期内有效,onDraw执行结束后Canvas会被系统回收,禁止缓存该指针,否则会触发野指针崩溃。 - 开启硬件加速时,Java层Canvas可能是
RecordingCanvas实例,对应的SkCanvas为录制画布,绘制指令会被序列化到RenderThread执行,操作该SkCanvas时不要随意修改matrix、clip等状态,避免破坏系统原有绘制流程。
内容的提问来源于stack exchange,提问作者Sourav Banerjee

