让AudioPlayerAgent支持动态播放列表(Windows Phone)

最近做的是有关背景音乐播放(调用系统播放器)的活儿,其中牵扯到Microsoft.Phone.BackgroundAudio.BackgroundAudioPlayer播放本地或远程音频时候的播放列表改动问题。如果希望播放系统Music + Videos中存储的音乐,那么直接在UI利用Microsoft.Xna.Framework.Media传递和播放音乐即可。但如果是BackgroundAudioPlayer和AudioPlayerAgent就不太好办。

可能有人会说这种问题还用得着单写一篇文章吗,AudioPlayerAgent当然支持动态列表了。但很可惜MSDN的官方How-To的例子就是硬编码的列表(还是本地音频),不然也不会在App Hub Forum上出现这样的问题(12)。

一般来说将播放列表传递给Agent有这几种方式:

  • IsolatedStorageSettings
  • 静态方法或属性(作为BackgroundAgent,实例化它没有意义)
  • IsolatedStorageFile

实验之后就会发现,前两种方法并不好用:虽然在UI线程中设置了列表或者通过静态方法、属性传递了列表,但启动后的Agent拿不到这些数据。就算是单写一个存储播放列表数据的静态类也是一样。那么留给我们的方法只有通过文件传递。

上面提到的那个帖子中,一位微软员工Mark Chamberlain给出的方案也是如此(全文请看原帖):

This is how I did it:
In background audio agent code:
OnUserAction.Play/SkipNext/SkipPrevious: Check if BackgroundAudioPlayer.Instance.Track == null, if yes then load playlist from IsoStorage into m_playlist and start playing first track, if no then play current/next/previous song from m_playlist
In the album viewmodel code (each time a user selects an album a new viewmodel gets created for this app):
OnInitialization:…
Another developer who reviewed this suggested that instead of calling ..Instance.Close(), which also releases all the background audio resources, a better approach is to set BackgroundAudioPlayer.Instance.Track = null, which stops and clears, but keeps the background resources allocated.

基本思路即UI存好文件,Agent初始化时候载入。任何和播放相关操作前播放列表如果为null则再次载入。

具体到我手头的例子,文件格式我直接用的Xml序列化,写起来简单。由于需要和UI保持实时,那么每修改一次播放列表(添加、删除、改动)都要保存,同时在Agent一边,任何和播放相关操作前则不仅检查是否列表为null,而是直接全部重新载入。类似于:

protected override void OnUserAction(BackgroundAudioPlayer player, AudioTrack track, UserAction action, object param) {
    var allTracks = TrackManager.GetAllTracks();
    _playList = new List();
    foreach ( TrackItem trackItem in allTracks ) {
    _playList.Add( 
            new AudioTrack( null, trackItem.Title, trackItem.Album, null, null, trackItem.Url, EnabledPlayerControls.Pause ) );
    }
    //...
}

代码写出来非常愚蠢,但目前为止找不到更好的方法。

有关dynamic_cast<>的可用性(基础差害死人)

一直以来,有关dynamic_cast<>这个转换所读到的书和材料消化后,都觉得结论是“拥有带有继承关系的对象指针转换,安全,会抛异常”。但事实上真的如此吗?看下面的代码:

class B1{virtual void foo(){}};
class B2{};
class D{};
D* pD=dynamic_cast(new B1()); // (1)
cout<<*(int*)(&pD)<(new B2()); // (2)
cout<<*(int*)(&pD2)<

可以看到B1、B2和D三个类没什么继承关系,但(1)处通过编译,(2)处提示:

cannot dynamic_cast `(((main()::B2*)operator new(1u)), (((*)  main()::B2()), ))' (of type `class main()::B2*') to type `class main()::D*' (source type is not polymorphic) 

即来源类型非多态。所以实际上并非是“有继承关系的对象指针”可用dynamic_cast<>转换,而是有vtable的类都可以转——只不过没继承关系的话转出来为NULL。
那么再想一下,既然提示了“多态”,也就是说关键点不在“继承”这里,所以形如下面的代码:

class X {};
class Y : X {};
Y* y = dynamic_cast(new X()); // failed!

虽然两个类有继承关系,但必然通不过编译,因为基类子类并未表现出多态,编译器不会多费劲来生成vtable。在文章多继承的虚指针中提到了dynamic_cast的内部实现:

void *FindCompleteObject (void *ptr )
{
     const _s_RTTICompleteObjectLocator *pCompleteLocator =GetCompleteObjectLocator (ptr );
     ptr =ptr -pCompleteLocator ->offset ;
     if (pCompleteLocator ->cdOffset ) ptr -=*(ptr -pCompleteLocator ->cdOffset );
     return ptr ;
}
void *__RTCastToVoid (void *ptr )
{
     if (!ptr ) return NULL ;
     return FindCompleteObject (ptr );
}

可以看到只有vtable存在,寻找Complete Object Locator的行为才有意义——虚表的第一项就是RTTI Complete Object Locator(RTTI Info),其中包含RTTI类型描述(Type Descriptor)、RTTI继承关系(Class Hierarchy)[2]。故言C++的RTTI特性也是以vtable的存在为基础,没有虚函数或其他virtual特性,RTTI不生效。
当然一般使用中其实理解成dynamic_cast<>用于向下转换带有继承关系的指针也没错,上面提到的这两个特例应该很少会出现在实际代码中吧。但此处犯错足见俺基础太差,离C++对象模型的深入理解还差得远。
p.s:真想把之前这类因为基础差犯2B错误的文章集结起来。最近几个月就写了不少。

参考资料

  1. 多继承的虚指针,lw02nju
  2. 浅议 Dynamic_cast 和 RTTI,RocZhang