Design and Implementation of Mobile Device-oriented Vector Drawing Platform
引用本论文: 张云贵. 面向移动设备的矢量绘图平台设计与实现[D]. 北京:北京理工大学软件学院, 2013.
本论文的相似度为0%,是源创论文。欢迎评阅讨论,请勿抄袭,如需更多资料请在博客留言。
如果在研究或论文中使用到,欢迎回复或私信你的学校、姓名、研究领域,并在论文中添加引用或致谢。感谢你对开放成果的尊重和鼓励。
第5章 Android绘图平台的实现
本章阐述了Android绘图平台的实现方法,主要是在跨平台内核的基础上实现Android画布适配器和视图适配器,对图形显示优化技术进行了实验研究。
5.1 开发环境
5.1.1 SWIG的工作原理分析
如图5‑1所示,借助于SWIG实现Android程序的Java代码通过JNI访问C++的类。在编译阶段SWIG工具从C++生成JNI的Java类文件和相应的C++实现文件,该实现文件与原有的C++实现文件一起通过NDK编译为本地动态库。
图5‑1 Android程序调用C++类的原理
如图5‑2所示,利用SWIG的Director特性,指定某个具有虚函数的C++类可重定位,然后再生成C++导出函数文件和JNI的Java类文件。在应用层中从对应的JNI类继承并实现其函数,在执行该C++类的虚函数时对应的Android函数就被执行。
图5‑2 Android类从C++类的虚函数重载的原理
图5‑2中,SwigDirector_SomeClass从C++的SomeClass派生,Android类从JNI的SomeClass类派生,在SwigDirector_SomeClass中通过调用JNIEnv类的CallStaticVoidMethod等函数实现在C++中调用Java的类函数,这样Android类中相应的重载函数便得到调用,实现使用Android SDK的Java类来扩展C++类。
5.1.2 SWIG的运行性能分析
TouchVG平台使用SWIG实现Android程序的Java类与内核的C++类之间的双向调用,即Java类通过JNI调用C++类、C++类利用Director特性回调Java类。
本文对这两种调用方式进行评测,结果见图5‑3。
图5‑3 SWIG在Android中的性能评测结果
图5‑3包含下列四个评测项目:
(1)回调绘图:Android程序通过JNI调用跨平台内核的一个测试函数,在该测试函数中多次回调画布适配器的绘制直线段的函数,在该绘制函数中使用android.graphics包绘图。其性能影响因素有JNI调用、回调和绘图。
(2)回调不绘图:与上一项目的差别是将画布适配器的绘制函数改为空实现,不受图形库的影响。其性能影响因素有JNI调用和回调。
(3)直接绘图:Android程序直接调用绘制直线段的函数,与JNI无关。
(4)正向调用:Android程序通过JNI多次调用跨平台内核的一个测试函数。其性能影响因素是JNI调用,与JNI回调及绘图无关。
评测结果表明,基于虚函数重定位技术的回调方式的性能与普通的JNI调用方式的差别较小,SWIG所增加的封装函数并不会使绘图性能明显下降。
5.1.3 开发方式
Android绘图平台的实现方式如图5‑4所示,编译得到的绘图平台JAR包和内核本地动态库可供应用程序使用。借助于SWIG的Director机制,使用Android SDK实现了画布适配器和视图适配器,实现对内核功能的扩展。
将跨平台内核使用NDK编译到本地动态链接库中,接口形式为JNI和封装类库。采用SWIG将C++类转换为JNI的Java类,SWIG所生成的C++导出函数文件与跨平台内核的代码文件一起编译为动态库。编译过程中使用Python脚本自动修正SWIG所生成的代码中的缺陷,并自动将包含中文字符的文件由UTF8临时转换为GBK编码以便正常编译转换。
图5‑4 Android绘图平台的实现方式
对于Android本地动态链接库的调试定位难题,利用NDK提供的日志输出C函数和库文件,通过输出日志文字的方式来解决。用该方法诊断出了SWIG引起的JNI内存问题所在位置,最终采用Python脚本自动修正SWIG所生成的文件缺陷。
5.1.4 开发工具
使用了下列工具分别在Mac OS X 10.7和Windows 7上开发Android绘图平台:
(1)Android开发包(the ADT Bundle)r21.1,可以在Eclipse中调试本地动态库。
(2)Android NDK r8e,用于开发本地动态库。
(3)SWIG 2.0.10,用于从C++头文件生成JNI的类文件和C++封装文件。
(4)Python 2.7,用于运行Python脚本自动修正SWIG所生成的文件缺陷。
(5)MSYS(Minimalist GNU for Windows)1.0,用于在Windows上模拟UNIX环境,执行Shell编译脚本。
5.1.5 SWIG编译配置
在touchvg.swig文件[1]中配置SWIG编译选项,主要配置内容如下:
(1)在文件前面定义下面两个宏:
SWIG_JAVA_NO_DETACH_CURRENT_THREAD
SWIG_JAVA_ATTACH_CURRENT_THREAD_AS_DAEMON
定义前者以便在每次调用本地代码后不与当前线程断开(使用SWIG的Director机制后,在本地函数调用结束时一些JNI对象还需要继续有效,不能与当前线程断开)。定义后者将JNI环境附加在守护线程上(默认是附加在界面主线程上,在Activity退出时可能会崩溃)。
(2)输出JNI_OnLoad函数。Dalvik虚拟机要求必须实现JNI_OnLoad函数,本平台仅简单返回JNI_VERSION_1_6,由SWIG生成的代码自动注册本地函数。
(3)指定GiCanvas和GiView需要生成重定向类,并导出相应的头文件。
(4)输出要在Android代码中使用的内核接口。例如,GiCoreView类。
(5)添加TmpJOBJ辅助类,在析构函数中自动释放JNI本地引用对象。SWIG生成的Director类中某些形参的本地引用对象没有释放,会因超出256个JNI引用对象的限制而溢出崩溃。例如,在GiCanvas的drawBitmap函数中,name字符串对象所对应的本地引用对象“jstring jname”在调用了NewStringUTF函数后没有调用DeleteLocalRef函数。
本文针对该问题提出的解决方法:将SWIG所生成的封装文件中的“jstring jname = 0”替换为“jstring jname = 0; TmpJOBJ jtmp(jenv, &jname)”,通过TmpJOBJ的析构作用自动调用DeleteLocalRef函数释放引用。使用Python脚本[2]自动进行替换SWIG生成的封装文件中的这类问题。
编写了Shell脚本(mk/swig.sh),用于运行SWIG工具生成JNI导出函数的封装文件(touchvg_java_wrap.cpp)和JNI类文件。JNI类的包名为touchvg.jni,其文件输出到工程的src/touchvg/jni目录下,将与视图适配器的代码(src/touchvg/view目录)共同生成为一个JAR文件。
5.1.6 NDK编译配置
Android绘图平台的代码目录结构见第20页的图3‑7。在工程的jni/Android.mk文件[3]中配置本地动态库的NDK编译选项,主要有:
(1)基于绝对地址$(LOCAL_PATH)/../../../core/include在LOCAL_C_INCLUDES中指定内核的头文件路径,基于相对地址 ../../../core/src 在LOCAL_SRC_FILES中指定跨平台内核的实现文件(*.cpp,使用绝对的路径无法编译)。
(2)因为SWIG的Director代码使用了RTTI运行时类型信息,所以在LOCAL_CFLAGS中指定-frtti选项。
(3)为了使用STL,在jni/Application.mk中指定“APP_STL := stlport_static”。
本文编写了Shell脚本(ndk.sh),在其中进入android\demo\jni目录自动运行ndk-build编译出本地动态链接库libtouchvg.so,在编译过程中自动应用Android.mk中的配置信息。Onur Cinar[4]介绍了在Android.mk中包含脚本的方法,可自动运行脚本。
本文在多个平台编译时发现Shell脚本文件应使用Unix行结束符(LF),不能是DOS结束符或Mac结束符,尽可能避免使用中文字符。
5.2 基于Android Canvas实现画布适配器
在第12页的2.2.3节介绍了Android二维绘图主要涉及的框架。本文主要基于两种视图类设计绘图视图类:android.view.View和android.view.SurfaceView,在绘图视图类中使用Android Canvas画布类(使用android.graphics包)渲染。
5.2.1 画布原语与Android Canvas的映射
本文基于android.graphics包设计画布适配器类touchvg.view.CanvasAdapter,该类实现touchvg.jni.GiCanvas中的画布原语函数,后者是通过SWIG从跨平台内核的GiCanvas接口自动生成的。在内核中调用画布接口GiCanvas的函数时,画布适配器将被回调执行,从而允许使用Android Canvas渲染。
画布适配器主要使用了android.graphics包中这些类:Canvas画布类访问绘图函数接口,Paint类指定颜色等绘图属性,Path类构建路径,Bitmap指定位图数据。在绘图视图的onDraw函数中将Canvas画布对象传入画布适配器,后续绘图将在该画布对象上进行。在离屏位图上渲染时,从位图构建画布对象,接着传入画布适配器。
因为Paint对象只能指定一个颜色,无法区分画笔颜色和画刷颜色,所以画布适配器针对画笔、画刷和文字显示分别使用一个Paint对象:mPen、mBrush、mTextPen。以显示一个红边蓝底的椭圆为例,先设置mPen的颜色为红色、mBrush的颜色为蓝色,然后分别使用mPen和mBrush作为参数绘制椭圆。为了让文字颜色和图形颜色同步,在setPen函数中同时设置画笔mPen和文字属性mTextPen的颜色。
这三种Paint对象的参数设置见表5‑1。
表5‑1 Paint对象的参数设置
画笔 |
画刷 |
文字 |
mPen.setAntiAlias(true) |
mTextPen.setAntiAlias(true) |
|
mPen.setDither(true) |
mTextPen.setDither(true) |
|
mPen.setStyle(STROKE) |
mBrush.setStyle(FILL) |
|
mPen.setPathEffect(null) |
mBrush.setColor(0) |
|
mPen.setStrokeCap(Cap.ROUND) |
||
mPen.setStrokeJoin(Join.ROUND) |
表5‑1中,画刷默认填充颜色为透明色,即不填充。画笔的默认线型为实线,线端为圆端,这样在绘制短线时更像一个圆点。为了让点线等虚线类型的空白间隙整齐,在setPen函数中对所有虚线类型设置平端的线端类型。
与iOS绘图平台的实现类似,Android画布适配器按表5‑2所示的映射方法实现了画布原语函数。由跨平台内核中的TestCanvas类生成和显示矢量图形和图像。
在实现这些函数时,本文对下列内容进行了特殊处理或总结。
(1)在View中调用画布适配器的clearRect函数,无法使指定区域透明,如图5‑5(i)所示。只能在原有图形基础上填充颜色,指定透明色将填充为黑色。在SurfaceView中调用画布适配器的clearRect函数,可以擦除指定区域内的图形,变为透明区域,如图5‑5(k)所示。
(2)当程序和视图使用了硬件加速特性后,调用clipPath会崩溃,其原因是在硬件加速时不支持clipPath函数。解决方法是在UnsupportedOperationException异常出现后将对应的视图的层类型设置为软件实现方式(LAYER_TYPE_SOFTWARE)。
(3)在SurfaceView视图中使用渲染线程连续绘图能够达到48~56FPS的更新速度,实验效果如图5‑5(n)所示。测试用例为绘制不断延长的三次贝塞尔曲线,测试条件为MOTO MZ606平板电脑(Android 4.0.3,1280×800)。
表5‑2 画布原语与android.graphics的映射
画布原语 |
测试号 |
Android函数对应关系 |
clearRect |
i k |
mCanvas.drawColor(mBkColor, Mode.CLEAR),需要设置剪裁区域 |
drawRect |
a |
mCanvas.drawRect,使用mPen和mBrush |
drawEllipse |
b |
mCanvas.drawOval,宽高不超过1时使用drawPoint |
beginPath |
多个 |
创建路径对象mPath |
moveTo |
多个 |
mPath.moveTo |
lineTo |
c |
mPath.lineTo |
bezierTo |
e |
mPath.cubicTo |
quadTo |
f |
mPath.quadTo |
closePath |
c |
mPath.close |
drawPath |
多个 |
mCanvas.drawPath(mPath, mBrush)、.drawPath(mPath, mPen) |
drawHandle |
g |
mCanvas.drawBitmap,在指定点显示 |
drawBitmap |
g |
mCanvas.drawBitmap,指定Matrix矩阵变换对象 |
drawTextAt |
h |
mTextPen.setTextSize、mCanvas.drawText,用到FontMetrics |
setPen |
多个 |
mPen.setColor、mPen.setStrokeWidth、mTextPen.setColor mPen.setPathEffect、mPen.setStrokeCap |
setBrush |
多个 |
mBrush.setColor |
saveClip |
m |
mCanvas.save(CLIP_SAVE_FLAG) |
restoreClip |
m |
mCanvas.restore() |
clipRect |
m |
mCanvas.clipRect |
clipPath |
m |
mCanvas.clipPath,硬件加速时需要将视图的层改为软件实现类型 |
drawLine |
d |
mCanvas.drawLine |
注:其中的测试号为图5‑5中的测试子图号。
图5‑5 Android画布适配效果
5.2.2 图像的显示和管理
在绘图视图中管理图像对象,画布适配器从视图获取图像。显示接口函数为:
void drawBitmap(String name, float xc, float yc, float w, float h, float angle)
其中,使用名称name标识图像对象,(xc,yc)为图像的中心显示位置,w和h为显示目标宽高,angle为旋转的角度(世界坐标系中的逆时针方向)。
图像绘制的基点在图像的左上角,画布的坐标系为ULO类型,绘制过程为:
(1)根据name从绘图视图获取Bitmap对象;
(2)计算变换矩阵:将显示基点由图像的左上角平移到中心,反向旋转angle角度(弧度转换为度),将宽高分别放缩到w和h,最后平移到(xc,yc)。
(3)使用此矩阵显示图像对象。
本文实验发现在显示大图片时,加载图片所需时间远大于显示图像的时间,因此减少图片加载次数能加快显示速度。本文采用下面两种方法进行图像管理:
(1)在绘图视图类中使用LruCache缓存图片。定义LruCache<String, Bitmap>类型的成员变量,以图像标识串(drawBitmap中的name)为键值管理图像对象。
(2)加载图片前先检查图片的宽高,如果太大就以降低采样率方式加载图片。
5.3 绘图视图的设计和实验
为了提高视图的显示质量和性能,本文针对View、SurfaceView进行了实验。
5.3.1 实现方式
绘图视图使用画布适配器CanvasAdapter绘图,由跨平台内核中的GiCoreView和TestCanvas类自动显示测试图形。绘图视图类的关系见图5‑6,实现方式说明如下。
(1)GraphView。从View派生,在onDraw中绘图,调用invalidate()重绘。在onDraw中调用内核视图的drawAll函数显示所有图形。在触摸响应函数中调用内核视图的onGesture函数传递手势动作,由后者在某个交互命令中调用视图的redraw等函数,这将回调到GraphView的视图适配器(ViewAdapter),后者调用视图的invalidate()标记需要重绘。
(2)面板表面视图。从SurfaceView派生。调用setZOrderOnTop(true)设置为面板窗口,显示于宿主窗口之上。调用getHolder().setFormat(TRANSPARENT)设置其Surface背景透明,以显示宿主窗口的内容。在Surface就绪和刷新显示时启动渲染线程,在绘图线程中获取画布绘图,由内核视图的drawAll函数显示所有图形。
(3)媒体表面视图。从SurfaceView派生。默认就是媒体窗口,显示于宿主窗口之下,自动在宿主窗口上设置透明区域以便让SurfaceView上的内容可见。在Surface就绪和刷新显示时启动渲染线程,在绘图线程中获取画布绘图。
(4)GraphViewCached。从View派生,使用一个位图缓存图形内容,在onDraw函数中显示该位图。
内核调用regenAll函数时销毁该位图,下次onDraw函数执行时重新生成位图。应用增量绘图技术,添加新图形后调用regenAppend函数,直接在该位图上绘制新图形,下次onDraw函数执行时显示有新内容的位图。
图5‑6 Android渲染视图的类关系
(5)静态View + 动态View。在布局视图中创建两个基于View的视图类(GraphView和DynDrawStdView),分别显示不变的图形和经常改变的内容,前者渲染的内容通常较多。
(6)面板表面视图 + View。在布局视图中创建GraphSfView和DynDrawStdView视图。在GraphSfView中显示静态图形,GraphSfView位于窗口顶端。在DynDrawStdView中显示动态图形。
(7)静态View + 面板表面视图。在布局视图中创建GraphView和DynDrawSfView视图。在GraphView中显示静态图形,在DynDrawSfView中显示动态图形。DynDrawSfView是面板表面视图,在子线程中获取画布绘图,每次刷新显示时启动渲染线程。
(8)面板表面视图 + 面板表面视图。在两个位于窗口顶端的视图类中分别显示静态图形和动态图形。
(9)媒体表面视图 + View。在GraphSfView中显示静态图形,GraphSfView位于根视图层次的底端,在DynDrawStdView中显示动态图形。
(10)媒体表面视图 + 面板表面视图。在两个基于SurfaceView的视图类中分别渲染静态图形和动态图形,静态图形在窗口底端渲染,动态图形在顶端渲染。
以上的视图类按表5‑3调用内核视图的显示函数,由GiCoreView显示图形。
表5‑3 Android视图类与内核显示函数的对应关系
视图类 |
对应的GiCoreView显示函数 |
视图类 |
GiCoreView函数 |
GraphView |
drawAll |
DynDrawStdView |
dynDraw |
GraphSfView |
drawAll |
DynDrawSfView |
dynDraw |
GraphViewCached |
drawAppend、dynDraw、drawAll |
5.3.2 实验结果
本文针对View、SurfaceView进行了上述十组实验,实验结果见图5‑7和表5‑4。实验条件为:Android 3.0、模拟器(320×480),其中使用较小分辨率是便于在本文中插入屏幕截图。在MOTO MZ606平板电脑(Android 4.0.3,1280×800)上实验后也得出相同的结论。
从这十组实验得到下列结论:
(1)普通的绘图方式基于View实现定制视图,在onDraw函数中使用Canvas进行绘图。该方式使用简单,适合绘制简单图形。缺点是绘图速度较慢,刷新一个视图会使同级的其他视图被动刷新,容易引起显示性能下降问题。
(2)交互式绘图显示速度快的方式有:使用增量绘图技术的普通视图(GraphViewCached);在SurfaceView中绘制动态图形的双层绘图视图。使用增量绘图技术的优点是可以只需要一个绘图视图,双层绘图视图的优点是可以在子线程中绘图,能提高刷新帧率。可以将两者的优点结合起来,在GraphViewCached中显示静态图形,在SurfaceView中绘制动态图形。
(3)如果要在SurfaceView中异步绘制静态图形,合适的使用条件有:a、与其他内容视图没有重叠区域;b、在窗口顶端透明显示,不要在此区域显示按钮等临时界面控件;c、在窗口底端显示,窗口里没有不透明的大面积界面元素。
图5‑7 Android渲染视图效果
表5‑4 Android渲染视图的组合实验情况
图号 |
视图搭配类型 |
显示速度和问题 |
a |
GraphView |
慢 |
b |
GraphSfView(面板表面视图) |
慢,图形遮挡按钮 |
c |
GraphSfView(媒体表面视图) |
慢 |
d |
GraphViewCached |
动态和静态绘图都很快 |
e |
静态View + 动态View |
慢,另一视图被动刷新 |
f |
面板表面视图 + View |
快,静态图形遮挡按钮和动态图形 |
g |
静态View + 面板表面视图 |
快 |
h |
面板表面视图 + 面板表面视图 |
快,动态绘图拖尾明显,遮挡按钮 |
i |
媒体表面视图 + View |
快,不透明视图会遮挡静态图形 |
- |
媒体表面视图 + 面板表面视图 |
快,不透明视图会遮挡静态图形 |
注:其中的图号为图5‑7中的子图号。
5.4 Android绘图平台的结构
5.4.1 静态结构
根据绘图视图的实验结果,Android绘图平台按图5‑8设计静态类结构(省略了跨平台内核的内部结构和SWIG的Director类),相应类的说明见表5‑5。
图5‑8 Android绘图适配模块的结构
表5‑5 Android绘图适配模块的类
类 |
含义和职责 |
GraphViewHelper |
面向应用程序的绘图封装接口类,提供常用API |
GraphViewCached |
显示静态图形的视图类,使用了基于缓存位图的增量绘图技术,负责触摸手势识别,委托内核的GiCoreView实现图形显示和手势操作 |
DynDrawSfView |
显示动态图形的SurfaceView视图类,委托GiCoreView显示动态图形 |
ViewAdapter |
视图适配器,允许内核回调Android视图,通知刷新显示 |
CanvasAdapter |
使用Android Canvas实现的画布适配器 |
GiCoreView |
跨平台内核的视图分发器,托管图形对象,分发显示请求和手势信息给图形列表和当前命令 |
应用程序使用绘图视图有两种方式:(1)仅使用GraphViewCached视图,适合图形量不太多的场合。(2)通过GraphViewHelper创建一个布局视图,自动创建GraphViewCached和DynDrawSfView视图,适合动态绘图帧率要求较高的场合。
5.4.2 应用效果
在Android绘图平台(属于TouchVG框架)中应用多层绘图技术分离静态图形视图和动态图形视图,提高了动态交互式绘图的回显速度。在静态图形视图中应用增量绘图技术,在连续绘制曲线图形时没有明显的拖尾现象。因此,绘图体验较流畅。
在跨设备平台的内核中使用绘图命令可以显示各种图形,在内核视图中使用仿射变换可以实现放缩显示。图5‑9展示了在不同Android版本的模拟器和平板电脑上的实际绘图效果。
图5‑9 Android综合绘图效果
5.5 本章小结
本章详细描述了SWIG在Android中的应用方法和扩展机制,针对出现的本地引用问题提出了修正方法。实验表明,SWIG所增加的封装函数并不会使绘图性能明显下降。描述了基于Android Canvas实现画布适配器的方式,实现了图形和图像的矢量化显示。画布适配器的单元测试使用了跨平台内核自动绘制图形,证明在内核中可以使用C++在Android上交互式绘图。
对普通视图、面板表面视图和媒体窗口进行了组合实验,总结出交互式绘图显示速度快、不出现遮挡问题的两种方式:使用增量绘图技术的单一视图方式;在SurfaceView中绘制动态图形的双层视图方式。
最后给出了Android绘图平台的设计结构和应用效果。
[1] 详细的SWIG编译选项见文件:https://raw.github.com/rhcad/vglite/master/android/demo/jni/touchvg.swig 。
[2] Python脚本见文件:https://raw.github.com/rhcad/vglite/master/android/demo/jni/replacejstr.py 。
[3] NDK编译配置文件见:https://raw.github.com/rhcad/vglite/master/android/demo/jni/Android.mk 。
[4] Onur Cinar. Pro Android C++ with the NDK. Berkeley: Apress, 2012