1. 项目概述:一场发生在2007年前后的移动开发路线之争
你要是现在打开搜索引擎搜“Windows Mobile开发”,大概率会看到一堆404链接、存档网页,或者被自动重定向到Android/iOS的教程页面。但就在十五年前,当诺基亚还在用Symbian拼杀、iPhone尚未问世、Android连影子都没有的时候,Windows Mobile是企业级移动设备上最主流的操作系统之一——银行外勤用它跑信贷审批,物流司机靠它扫描条码,医院护士拿它查电子病历,工厂巡检员用它采集设备数据。而支撑这一切应用落地的,正是Native C++和.NET Compact Framework这两条截然不同、又彼此缠绕的技术路径。
我从2005年开始做Windows Mobile平台的嵌入式应用,最早在HTC TyTN(俗称“钻石机”)上调试串口通信驱动,后来给某省电力公司写过一套离线抄表系统,全程跑在ARMv4平台的工业PDA上。那会儿没有NuGet,没有Visual Studio的智能提示,没有自动内存管理,也没有现成的蓝牙SDK——所有东西都得自己抠寄存器、查头文件、手写CEGUI控件、反复烧写NK.bin验证启动流程。正因如此,我对Native C++和.NET Compact Framework之间的张力有着切肤之痛:不是教科书里冷冰冰的“托管vs非托管”概念对比,而是凌晨三点盯着CELOG日志抓内存泄漏时,一边骂着CLR的GC策略不透明,一边又不得不承认用C#三分钟写出一个带Web Service调用的配置界面,确实比用WTL手绘滚动列表快了十倍。
这个项目标题里的“PK”,不是竞技场上的胜负对决,而是一场持续数年的工程权衡实践。它不关乎语言优劣,而关乎资源约束下的现实选择——你的设备是64MB RAM还是128MB?CPU主频是200MHz ARM9还是400MHz XScale?用户是否接受首次启动多花2.3秒等待JIT编译?有没有硬件厂商只提供C风格的私有API DLL?这些具体到字节、毫秒、毫瓦的问题,才是决定你该敲 #include <windows.h> 还是 using System.Net; 的真实判据。接下来的内容,不会复述MSDN文档,也不会堆砌术语定义,而是把当年我们团队在真实项目中踩过的坑、记下的笔记、压在键盘下面的便签纸,原样摊开给你看。
2. 技术底座解构:为什么必须理解PE/COFF与IL/JIT的本质差异
2.1 Native Code的物理世界:从源码到裸金属的每一步都由你掌控
Native C++在Windows Mobile上的本质,是直接与Windows CE内核对话。这里的“Native”,不是营销话术,而是字面意义的“原生”——它生成的可执行文件(.exe或.dll)是标准的 Portable Executable (PE) 格式,但针对嵌入式场景做了深度裁剪,更准确地说,是 CE-PE 变体。它继承自桌面Windows的PE结构,但去掉了重定位表(relocation table)的大部分字段,精简了导入地址表(IAT),并强制要求所有模块使用固定基址加载(Base Address)。这是为了适配CE系统有限的虚拟内存管理能力——毕竟在2006年的iPAQ hx4700上,整个用户空间只有32MB虚拟地址空间,其中还被系统保留了一半。
我至今记得第一次用 dumpbin /headers 查看自己编译的DLL时的震撼:Section Headers里赫然写着 .text 段的VirtualSize是0x1A2C,而RawSize是0x1A00,两者差值28字节,正是PE头填充的对齐空隙。这28字节在桌面系统里微不足道,但在CE设备上,它意味着28字节的ROM空间被永久占用。而当你用 depends.exe (CE版依赖查看器)打开这个DLL,会发现它只依赖 coredll.dll 和 aygshell.dll 两个系统模块——没有 msvcr71.dll ,没有 mscorlib.dll ,因为所有CRT函数(如 malloc 、 printf )都被静态链接进了你的二进制文件。这就是Native的代价与荣光:你放弃了一切运行时便利,换来了对每一个字节的绝对主权。
提示:在Windows Mobile 5.0之后,微软引入了 Shared Heap 机制,允许多个进程共享同一块堆内存。但Native程序默认仍使用私有堆(Private Heap),除非你显式调用
HeapCreate(HEAP_SHARED, ...)。很多内存泄漏问题,根源就在于开发者误以为new分配的内存会自动被系统回收——实际上,在CE环境下,delete失败或遗漏,会导致私有堆碎片化,最终触发ERROR_NOT_ENOUGH_MEMORY,而此时设备总内存可能还有20MB空闲。
2.2 .NET Compact Framework的抽象层:IL如何在资源受限设备上“翻译”执行
.NET Compact Framework(.NET CF)绝不是桌面.NET Framework的简单缩水版。它的设计哲学是“功能够用,体积最小”,为此做出了大量激进取舍。以.NET CF 2.0为例,其Runtime(clr.dll)体积控制在1.8MB以内,而同期桌面版CLR已超10MB。这种压缩不是靠删代码,而是重构整个执行模型:
-
JIT编译器被彻底重写 :桌面版JIT能生成高度优化的x86指令,而.NET CF的JIT(称为 Tiny JIT )只做最基础的IL到ARM/SH/X86机器码映射,几乎不进行循环展开、内联优化等高级操作。它甚至放弃了完整的异常处理栈展开(stack unwinding),转而采用基于SEH(Structured Exception Handling)的轻量级实现。
-
垃圾回收器(GC)采用分代+标记清除(Generational Mark-Sweep) :但仅设两代(Gen 0和Gen 1),且Gen 0大小固定为256KB。这意味着,一旦你的对象频繁创建销毁,Gen 0很快填满,触发GC频率极高。我们曾有个物流APP,每秒解析10条GPS NMEA语句,结果GC每3秒就停顿一次,UI卡顿明显。解决方案不是调大Gen 0——CE内存不允许——而是改用对象池(Object Pool)复用
StringBuilder和byte[]缓冲区。 -
Base Class Library(BCL)被严格筛选 :
System.Reflection.Emit、System.Threading.Thread(仅支持Thread.Sleep和Thread.Abort)、System.Security.Cryptography(仅含MD5/SHA1)等高开销类库被移除。最典型的例子是网络编程:.NET CF 2.0的System.Net.Sockets不支持异步Socket(BeginConnect/EndConnect),所有网络IO都是同步阻塞的。这意味着,如果你用HttpWebRequest下载一个5MB文件,主线程会完全冻结,直到传输完成或超时。这不是Bug,而是设计选择——为避免线程调度开销吞噬宝贵的CPU周期。
注意:.NET CF的“跨平台”是带引号的。虽然IL代码理论上可在ARM/MIPS/x86上运行,但实际部署时,你必须为每个目标平台安装对应版本的.NET CF Runtime。AR


419

被折叠的 条评论
为什么被折叠?



