最近由于需求,买了一个Xtion Live Pro,因此必须使用OpenNI进行体感开发了。很久没关注了,现在才发现OpenNI已经更新到了2.1beta,接口有非常大的调整,因此过去的很多C#Wrapper都不能用了,而且目前网上还没有针对新版本的Wrapper。用C++开发虽然灵活强大,但还是希望借助WPF或WinForm的优势进行软件开发,因此打算自己实现一个Wrapper,同时希望这个Wrapper的接口和Kinect SDK的接口尽可能相似,能用于任何.Net项目。
这里记录一下实现过程中学到的和需要注意的东西。
一、P/Invoke:
在C#与C/C++编写的Dll进行互操作时,Pinvoke是避免不了的。在调用C/C++函数时,一些参数是指针,在C#当中可以使用以下一些方法:
1、使用ref关键字:
[DllImport("OpenNI2.dll", CallingConvention = CallingConvention.Cdecl)] public static extern OniStatus oniDeviceGetInfo(IntPtr device, ref OniDeviceInfo pInfo);如上面的pInfo参数,需要一个指向OniDeviceInfo结构的指针,ref关键字的作用是传递引用,本质上就是指针。
使用时,在传递的参数前加上ref即可,如下:
OniDeviceInfo info = new OniDeviceInfo(); oniDeviceGetInfo(_deviceHandle, ref Info);
2、使用StructureToPtr方法:
如果参数是一个结构体指针(像上面那样),还可以使用Marshal类当中的StructureToPtr方法,该方法需要在非托管内存区分配一块内存,并将托管数据复制到非托管区,并返回一个指向非托管区的指针,当然,使用完之后要释放这块非托管内存。
VideoMode mode = new VideoMode {Fps = 30}; IntPtr pMode = Marshal.AllocHGlobal(Marshal.SizeOf(mode)); //分配非托管内存 Marshal.StructureToPtr(mode, pMode, false); oniStreamSetProperty(_streamHandle, 3, pMode, Marshal.SizeOf(mode)); Marshal.FreeHGlobal(pMode); //释放非托管内存
当然最直接也是最接近原函数接口的方法就是使用真正的指针。
unsafe { VideoMode* pMode = &mode; OniStatus oniStatus = oniStreamSetProperty(_streamHandle, 3, pMode, Marshal.SizeOf(mode)); }如果要传递的是类的指针,还需要用fixed关键字将类固定。
unsafe { fixed (NiteUserTrackerCallbacks* pHandler = &_callbackHandler) //固定对象 { niteRegisterUserTrackerCallbacks(_streamHandle, pHandler, IntPtr.Zero); } }
1、使用lock语句:
lock语句需要一个对象作为锁(任何对象都可以,哪怕是一个Object类),所有需要同步的线程,在访问数据前都要使用lock语句锁住相同的锁。
lock (_locker) { if (_streamHandle != IntPtr.Zero) { NiteWrapper.niteShutdownUserTracker(_streamHandle); _streamHandle = IntPtr.Zero; } IsOpened = false; }如上,lock语句中的代码同一时刻只能有一个线程执行(前提是使用的_locker指向同一对象)。
2、使用volatile关键字:
如果只是为了同步变量,比如一个列表类(List),可以在申明该变量时加上volatile关键字,如下:
private volatile List<ColorFrame> _frameList = new List<ColorFrame>();这样在访问该变量时,即使不使用lock语句,也能避免多线程访问。
三、线程的退出:
对于一个轮询线程,最好的写法如下:
while (!_shouldStop) { //Do anything you want. }需要退出时,直接将_shouldStop变量设置为true即可,如下:
public void Stop() { _shouldStop = true; //Do something else. }
_newColorThread = new Thread(NewColorFrameProcess) {Name = "Color Thread", IsBackground = true}; _newColorThread.Start();
这里再记一个奇怪的事情,如果对后台线程(IsBackground = true)使用阻塞等待,如调用线程的Join方法,整个程序会挂掉,不知这是否是.Net的bug。
后台线程等到新的帧到来时,就会触发事件,如果直接在后台线程上抛出,用户注册事件响应程序也会在该线程上执行,要是响应程序中有访问UI元素的语句,程序必挂(.Net不允许非UI线程访问UI元素)。虽然可以使用Invoke方法解决该问题,但使用这个方法时还必须拿到待访问元素的引用或UI线程的Dispatcher,对于一个可用于任何.Net程序的类库,这显然不是个好方法。
目前发现的最好的方法是使用SynchronizationContext类。该方法有一个静态熟悉Current,可返回当前所在线程的Context。每次用户注册事件响应程序时,就应该将用户所在线程的Context保存下来,如下:
public void AddHandler(EventHandler<T> listener) { if (listener == null) return; lock (_locker) { if (_disposed) return; _listenerList.Add(new ContextListenerPair<EventHandler<T>>(listener, SynchronizationContext.Current)); //保存当前Context和Handler } }其中的ContextListenerPair是自己实现的一个类,用于保存Context和事件响应程序的Handler。
当需要向UI线程抛出事件时,可以使用Context的Send或Post方法。Send方法是同步的,即等到UI线程执行完事件响应程序后才返回,而Post方法是异步的,它将事件消息投放到UI线程的消息队列之后就立即返回。下面以Post方法为例:
EventHandler<T> listener = contextPair.Listener; SynchronizationContext context = contextPair.Context; if (context == null) { listener(sender, eventArgs); //如果没有Context,直接抛出事件 } else { context.Post(PostDelegate, new ContextArgsWrapper<T>(listener, sender, eventArgs)); }Post方法的第一个参数是需要在UI线程上执行的函数,第二个参数是该函数要用到的参数。在PostDelegate函数中,再进行事件抛出,就可以起到事件在UI线程上执行的目的,如下:
private void PostDelegate(object arg) { ContextArgsWrapper<T> wrapper = arg as ContextArgsWrapper<T>; wrapper.Listener(wrapper.Sender, wrapper.Args); //抛出事件 }