移动行业信息化-编写安全的SymbianC游戏代码_第1页
移动行业信息化-编写安全的SymbianC游戏代码_第2页
移动行业信息化-编写安全的SymbianC游戏代码_第3页
移动行业信息化-编写安全的SymbianC游戏代码_第4页
移动行业信息化-编写安全的SymbianC游戏代码_第5页
已阅读5页,还剩31页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

移动行业信息化ー编写安全的SymbianC++游戏代码摘要Symbian游戏是运行在手机上的游戏,它不能干扰手机正常的通讯功能,对操作系统和其它应用程序必须友善。而在首次编写SymbianC++游戏时,我遇到了无数奇怪的问题,其中大部分问题出在内存极端不足,打开太多应用程序,屏幕保护探出,接到短信、电话等特殊情况下。本文献给使用NokiaSymbian60SDK各个版本开发游戏软件的程序员。虽然本文主要是针对游戏软件,但是大部分内容对一般应用软件也同样适用。1,声明为了避免良心的谴责,首先我必须承认一点,我本人并不是靠SymbianC++糊口。除了forum,上的文章和SDK,我也没有看过任何关于Symbian的书籍。只是偶然的,我在天津猛玛游戏公司(www.mammothworld,com)认识并接触了Symbian〇我从零起步,写出了一个蹩脚的Symbian游戏引擎并在3650、7650上开发了一些游戏。所以我对Symbian的掌握完全是出于自己的猜测和理解,虽然

本文缺乏权威,但至少都是经验之谈,容易理解。概述Symbian游戏是运行在手机上的游戏,它不能干扰手机正常的通讯功能,对操作系统和其它应用程序必须友善。而在首次编写SymbianC++游戏时,我遇到了无数奇怪的问题,其中大部分问题出在内存极端不足,打开太多应用程序,屏幕保护探出,接到短信、电话等特殊情况下。其实如果养成严谨的代码风格,进行足够的错误处理,大部分问题本可以避免。为了解决它们,我很是花了一番功夫,所以在此把我的一些教训、经验写出,希望大家能避免犯同样的错误。如果你不是业余爱好者,而是为ー个认真的开发商工作,特别是如果你的产品需要通过SymbianSigned认证(),你就必须更加小心的对待本文提出的问题。SymbianSigned是ー个针对Symbian应用程序的认证,想要通过它,你的应用程序必须通过一系列严格的测试。认证对应用程序的文件管理、内存使用、系统事件响应、网络、资费和私人数据等都有一定的要求。如果想了解SymbianSigned认证的详细内容,可以去它们的网站下载白皮书。

异常处理虽然我们都知道任何ー个new(ELeave)或者带有L后缀的函数都可能抛出异常,但是很多业余的爱好者还是会忽视SymbianC++中异常处理的重要性。虽然有些函数只有在极其罕见的情况下オ会抛出异常。但不是危言耸听,如果你不写代码捕捉并处理它们,应用程序就会遇到〃系统错误”。普通C++使用throw抛出异常。异常抛出后,栈会不停回滚,直到遇到最近ー层catch为止。SymbianC++中的异常处理不使用try-catch和throw。但是它的处理机制和标准C++很是类似,区别仅仅是它只能抛出ー个整数错误码,而不是ー个任意对象。我将从异常的抛出、捕捉、处理三方面讲解这部分内容。1,抛出异常SymbianC++中,有下面几种情况下会抛出异常:使用静态函数User::Leave抛出异常。这个函数就是最基本的异常产生函数。下面讲的其它抛出方式都可以转化为User::Leave〇使用静态函数User::LeaveIfError把错误码转化为异常。有些函数比如CFbsBitmap::Create()有一个Tint的返回值。如果遇到错误,这些函数就会返回非KErrNone的错误值。此时,可以使用LeavelfError把这个返回值转化为异常。比如:User::LeavelfError(bmp->Create(iSize,EColor4k);其实

LeavelfError就是if(returnValue!=KErrNone)User::Leave(returnValue);使用new(ELeave)申请内存。如果没有足够内存可用,此操作产生一个KErrNoMemory异常。比如TText8*p=new(ELeave)TText8[20];相当于TText8*p=newTText8[20];if(p==NULL)User::Leave(KErrNoMemory);调用带有L后缀的函数。Symbian系统的命名规范中要求,每ー个可能Leave的函数都要有后缀L。包含有带L的内层函数调用的外层函数也必须加上L。这类函数中最常见的就是NewL,NewLC和ConstructLo这个规范比你想象的要重要。因为它给其他程序员ー个暗示,提示他们对这些函数进行保护。1.3.2.捕捉异常类似标准C++的catch语句,SymbianC++的TRAP关键字可以对一个可能产生异常的函数进行保护,并且捕获到异常值。比如:TinterrorCode;TRAP(errorCode,SomeDangerousFuncL());/Z保护执行SomeDangerousFuncL()函数if(errorCode!=KErrNone)!//捕捉到了一个异常,在这里添加处理异常的代码类似的TRAPD省去了你声明一个局部变量的麻烦。头两行代码可以简

写成:TRAPD(errorCode,SomeDangerousFuncL());1.3.3.处理异常对于不同的异常当然有不同的处理方法(废话:-))。我们以最常见的捕获到代表内存不足的KErrNoMemory异常为例讲解。注意在Container,AppUi等类的构造过程中,你不需要加入对内存不足的保护。因为这一切系统已经为你做好了。系统会弹出一个对话框报告内存不足。根据你的操作系统版本不同,这可能是中文的,也可能是英文或者其它语言的。如果你不信,可以在AppUi或者Container的ConstructL中写一行User::Leave(KErrNoMemory)试试看。我试验的结果如下:除了上面说的特殊情况,你可以简单的弹出ー个对话框,告诉用户没有足够的内存运行程序,并且安全的关闭程序。比如我的游戏程序就是这样处理的:voidCStageManager::DoGameFrame()

TRAPD(error,DoGameFrameProtectedL());if(error==KErrNoMemory)(StopGame();m_noMemoryDlg->ExecuteLD(R_KEY_INVALID_DIALOG);Exit();//CallCAknAppUi::RunAppShutter()}elseif(error!=KErrNone){User::PanicGL(/zSomeothererror."),error);})其中noMemoryDlg是直接或者间接在Container的ConstructL中创建的://inheaderfileCAknQueryDialog*mnoMemoryDlg;//somewhereinConstructLTBuf<128>errMsg;_LIT(formater,"Notenoughmemory.Pleaseclosesomeapplications.");errMsg.Copy(formater);

mnoMemoryDlg=new(ELeave)CAknQueryDialog(errMsg,CAknQueryDialog::EErrorTone);当然,你也可以制作一个精美的图片来报告内存不足,等待用户按任意键再退出。不过载入这个图片也可能会失败,所以至少在这个图片成功载入之前,你还是需要系统对话框来报告的。值得一提的是,你不一定需要退出程序,或者你可以稍后重试申请内存,幸运的话,没准第二次就能成功。这是因为Symbian系统会在内存不足时自动关闭ー些应用程序。我觉得这是Symbian系统ー个比较奇怪的设计。通常应用程序在AppUi的HandleCommandL中会响应EEikCmdExit消息,并且调用CAknAppUi::Exit()函数(如下代码)。这使得应用程序可以在应用程序管理器中用C键结束掉。这也使得Symbian操作系统有机会在内存不足时通过这个渠道自动关闭ー些应用程序。// //CFlyAppUi::HandleCommandL(TintaCommand)//takescareofcommandhandling// //voidCFlyAppUi::HandleCommandL(TintaCommand)switch(aCommand)

caseEEikCmdExit:Exit();break;//TODO:AddYourcommandhandlingcodeheredefault:break;})坦白说我没有尝试过重试申请内存这个办法,不过我想是可行的。1.3.4.栈回滚和对象的安全析构上面说到在遇到某些异常时,你可以选择弹出对话框并且结束程序,其实这会比你想象的要困难ー些。因为C++可不像Java那样有托管堆进行垃圾收集。不过好在C++栈会自动回滚,栈上的对象会被销毁。如果你此时调用CAknAppUi::RunAppShutter()结束程序,那么AppUi,Container的析构函数会依次被调用,引起你自己创建对象的析构函数也依次被调用。那么堆上的对象也要被销毁。可是,请记住,异常随时随处可能发生,使对象处于ー种"半构造"的状态。此时析构函数被调用可能会造成对无效指针的访问错误。请看下面这个例子,它犯了两个常见的错误:classBadExample

protected:TText8*m_pBuf;TText8*m_pBuf2;public:staticBadExample*NewL(){BadExample*self=new(ELeave)BadExample();self->ConstructL();returnself;}voidDeleteBuf(){deletempBuf;}voidRebuildBufLO{mpBuf=new(ELeave)TText8[256];}private:BadExample();deletempBuf;deletem_pBuf2;}voidConstructL(){mpBuf=new(ELeave)TText8[256];m_pBuf2=new(ELeave)TText8[256];});假设我们在AppUi的ConstructL中使用BadExample::NewL()来构造对象,在AppUi的析构函数中delete这个对象。下面我们分析一下可能遇到的问题:首先,在函数NewL中,self指针没有被保护,试想如果self->ConstructL()一句抛出异常。那么这个self指针指向的对象就没有return给外界(也就是AppUi),这个对象就永远“丢失了”,造成了内存泄露。正确的做法是使用CleanupStack对它进行保护。Cleanupstack至少能保证在程序退出时压入其中的对象都能销毁。staticBadExample*NewL()BadExample*self=new(ELeave)BadExample();

CleanupStack::PushL(self);self->ConstructL();CleanupStack::Pop();returnself;)但是注意,此处还有一个微妙的内存泄露。仔细看看CleanupStack::PushL()的声明:IMPORT_CstaticvoidPushL(TAny*aPtr);IMP0RT_CstaticvoidPushL(CBase*aPtr);IMPORTCstaticvoidPushL(TCleanupItemanltem);如果传入的指针是CBase指针,那么CBase的虚析构函数(virtual〜CBase())就能保证对象在销毁时正确的调用析构函数。可是本例中BadExample不是从CBase中派生,那么对象只能做很有限的销毁,根本不会调用析构函数。所以,如果ConstructL是由于第二个内存申请m_pBuf2失败,那么m_pBuf申请的内存就永远不会回收。所以正确的做法是,让BadExample从CBase派生。classBadExample:publicCbase其次,我们并没有为m_pBuf和m_pBuf2赋初值,在Release版中他们的值是随机的。那么,如果m_pBuf2的申请失败,析构函数还是会执行deletem_pBuf2,试图删除ー个无效指针。正确的做法是在构造函数中为m_pBuf和m_pBuf2赋初值NULL〇因为标准C++规定,deleteー个空指针不做任何操作。不过实际上,如果对象从CBase

派生,这ー步是没有必要的,因为CBase能保证派生类的成员变量在构造时自动清零。最后,动态的使用DeleteBuf和RebuildBufL是不安全的。如果你先用DeleteBuf删除了这个对象,那么m_pBuf就是ー个坏指针。可是紧接着的RebuildBufL可能会失败。此时如果析构函数被调用,还是会产生delete无效指针的错误。正确的做法是,在DeleteBuf中,把m_pBuf设为NULL〇总结上面说到的几点,完整的安全的代码是:classBadExample:publicCBaseIprotected:TText8*m_pBuf;TText8*m_pBuf2;public:staticBadExample*NewL(){BadExample*self二new(ELeave)BadExample();CleanupStack::PushL(self);self->ConstructL();CleanupStack::Pop();returnself;

voidDeleteBuf()deletempBuf;m_pBuf=NULL;}voidRebuildBufLO(mpBuf=new(ELeave)TText8[256];}private:~BadExample(){deletempBuf;deletem_pBuf2;}voidConstructL(){mpBuf=new(ELeave)TText8[256];m_pBuf2=new(ELeave)TText8[256];

1.4.安全的图像引擎SymbianC++游戏的2D图像显示部分一般由下面几个类组成:图像一封装了一个CWsBitmap。是基本的图片资源。支持图像之间的各种贴图和混合操作。双缓冲一一个和屏幕分辨率、色深相等的图像。直接写屏支持ー复合一个CDirectScreenAccess对象,实现MDirectScreenAccess接口。负责直接写屏的安全处理。比如来电、屏保时适时的停止和开启直接写屏与游戏逻辑。绘图类ー负责在图像中绘图。它不是对Gc的封装,而是通过直接修改图像内存区进行绘图。位图字体类ー使用预先创建的位图资源写字。如下图就是ー个预先创建的位图资源。优点是速度快,缺点是无法支持大字符集合,比如中文。字体缓冲区类ー还是使用Gc的DrawText函数绘制文字。但是同时用ー张位图作为ー个缓冲区存储最近绘制的文字。既能支持大字符集合,速度也很快。如果需要学习图形和直接写屏的基础,请参考ProgrammingGamesin

C++vl.0(www.forum,/main/1.6566.21.00.html)〇本文主要针对图像类和直接写屏类讲几个容易被忽略的问题。1.4.1.图像类的直接内存访问贴图是2D游戏最主要的画面操作。为了实现快速的贴图,或者实现某种混合效果,就不能再使用CFbsBitGc的BitBlt或者BitBltMasked进行贴图,而必须自己得到图片的内存地址,直接读写其中的数据。在读写图片内存地址的过程中,有几点需要加以注意。首先,只有当源图片和目标图片色深相等时,オ更容易进行贴图操作。所以,再载入图片的过程中,我习惯把非4k色的图片转化为4k色。之所以选择4k色是因为它也是后台缓冲区的色深。下面的代码通过转换可以保证iImage是4k色的图像。//Makesurethatwehavea4KcolordepthimageiniImageif(iImage->DisplayMode()!=EColor4K){//Create4kcolorimageCFbsBitmap*image=new(ELeave)CWsBitmap();CleanupStack::PushL(image);User::LeavelfError(image->Create(iSize,EColor4K));//CreatedeviceCFbsBitmapDevice*device=CFbsBitmapDevice::NewL(image);

CleanupStack::PushL(device);CFbsBitGc*gc;User::LeavelfError(device->CreateContext(gc));CleanupStack::PushL(gc);//Bitbittonewcolordepthgc->BitBlt(TPoint(0,0),iImage);//Destroycontextanddevice;CleanupStack::PopAndDestroy();//gcCleanupStack::PopAndDestroy();//deviceCleanupStack::Pop();//imagedeleteiImage;iImage=image;}其次,Symbian系统在内存匮乏时会进行碎片整理。所以如果简单的用CFbsBitmap::DataAddress获取内存首地址并开始读写,那么可能在你读写的过程中,图片已经被悄悄的移动了位置,你读写的就是ー块无效的内存区域。解决这个问题的办法是在获取首地址前,必须先锁定图像内存区域。在高版本的60系列SDK中(比如2.0,2.1),有LockHeap和UnlockHeap函数可以完成这个操作。但是在低版本的SDK中(比如0.9,L0),这两个函数是私有的。我们必须通过TBitmapUtil锁定内存。但是不一定必须使用TBitmapUtil的

SetPixel和GetPixel函数进行位操作。下面是最基本的没有关键色和Alpha通道的简单贴图代码。voidCimage::RenderToBitmapL(CFbsBitmap*aBmp,TPointaPos,constTRect&aRect)I//在此计算贴图目标矩形区域/Z代码略去/Z没有关键色和蒙板的最简单、最快情况if(!iKey&&iMask==NULL){/Z锁定TBitmapUtilbmpUtil1(ImageLO);TBitmapUtilbmpUtil2(aBmp);bmpUtill.Begin(TPoint(0,0));bmpUtil2.Begin(TPoint(0,0),bmpUtill);/Z获取首地址TUintl6*addr2=(TUintl6*)ImageL()->DataAddress();//sourceimageTUintl6*addr=(TUintl6*)aBmp->DataAddress();//targetbmpTintline=aBmp->ScanLineLength(

aBmpー〉SizeInPixels().iWidth,EColor4K)/2;Tintline2=iImage-〉ScanLineLength(//linelengthin16bitwordiImage-〉SizelnPixels().iWidth,EColor4K)/2;/Z计算扫描持续量和跳跃量Tintjump=line一rectw;Tintlasting2=rectw;Tintjump2=line2一lasting2;/Z获取贴图首地址TUintl6*p=addr+fromY*aBmp->SizeInPixels().iWidth+fromX;TUintl6*p2=addr2+line2*recty+rectx;//ThefirstpixeloutofinterestTUintl6*p2end=p2+line2*(toY-fromY-1)+lasting2+jump2;/Z开始扫描while(p2!=p2end)

/Z开始ー个扫描行TUintl6*p2endline=p2+lasting2;while(p2!=p2endline)(〃复制一个像素*p=*p2;/Z移动到下ー个像素p++;p2++;}//跳到下一行P+=jump;p2+=jump2;)/Z解锁bmpUtil2.End();bmpUtill.End();return;/Z其它情况。有关键色等等.最后告诉大家几个优化的小窍门:

使用While循环直接把指针的比较作为循环结束条件。不要再多用ー个整数来控制循环。贴图是个两重循环,如果你的代码需要判断是否支持关键色和Alpha通道等,尽量把判断外移到循环之外。每个象素都进行好几个if判断的开销太不值得。比如上面的代码,处理最简单的情况时,while循环内ー个if都没有。4k色时,RGB内存排列如下图。所以未被使用的4位正巧可以用来存储alpha通道。1.4.2.直接写屏和特殊系统事件游戏软件一般用CDirectScreenAccess进行直接写屏。大家都知道,WindowServer会在需要停止直接写屏时回调MDirectScreenAccess::AbortNow接口函数,在可以重新启动时回调MDirectScreenAccess::Restart接口函数。可是具体在这两个函数中做什么,SDK没有过多的介绍。我在此说一下我的做法。如果你合理的处理了这两个函数,就可以轻松应对来电、屏保、程序切换等事件。我们先说AbortNow,它的处理比较简单。你之需在其中停止驱动游戏逻辑的计时器(一般是个CPeriodic对象),停止声音模块(一般是ー个CActive任务)就可以了。值得费些力气的是Restart函数,它并不是在应用程序回到前台,并

且可以进行全屏直接写屏时オ被回调。所以不能在此时武断的恢复游戏逻辑,开始游戏。首先,你要调用CDirectScreenAccess::StartL()恢复直接写屏。但是必须给这个函数加上TRAP保护。因为它很可能抛出KErrNotReady异常。如果遇到这个异常,那你就直接返回好了,因为直接写屏此时并不能开始。接下来你需要检查一下绘图区域,看是否整个屏幕都可以被使用。如果不是,那也无需启动游戏逻辑,只需要用最后保留的后台缓冲区的内容更新直接写屏区域即可。第三种情况,如果直接写屏成功启动,并且整个屏幕都可以被绘制,オ启动游戏逻辑,启动声音等其它模块。完整的代码如下:voidCEngine::AbortNow(RDirectScreenAccess::TTerminationReasons/*aReason*/)I//Canceltimeranddisplayif(iGameTimer->IsActive())iGameTimer->CancelTimer();if(!iGameWorldPaused)(iGameWorldPaused=ETrue;iGameWorld->PauseGame();//Pauseaudiostreametc.

iPaused=ETrue;voidCEngine::Restart(RDirectScreenAccess::TTerminationReasons/*aReason*/)JTRAPD(error,SetupDirectScreenAccessL());switch(error){caseKErrNone:break;caseKErrNotReady:if(iDirectScreenAccess->IsActive())iDirectScreenAccess->Cancel();if(iGameTimer->IsActive())iGameTimer->CancelTimer();if(!iGameWorldPaused)(iGameWor1dPaused=ETrue;iGameWorld->PauseGame();

return;default:User::Panic(_L(wSetupDSAError"),error);}if(iPaused){if(iGameDawingArea==iRegion->BoundingRect())(iPaused=EFalse;if(!iGameTimer->IsActive())(iGameWorldPaused=EFalse;iGameWorld->ResumeGame();iGameTimer->Restart();})elsePauseFrame();

elseif(!iGameTimer->IsActive())4iGameTimer->Restart();voidCEngine::SetupDirectScreenAccessL()(//InitialiseDSAiDirectScreenAccess->StartL();//GetgraphicscontextforitiGc=iDirectScreenAccess->Gc();//GetregionthatDSAcandrawiniRegion=iDirectScreenAccess->DrawingRegion();//SetthedisplaytocliptothisregioniGc->SetClippingRegion(iRegion);

voidCEngine::PauseFrame()//Forcescreenupdate:thisisrequiredforWINS,butmay//notbeforallhardware:iDirectScreenAccess->ScreenDevice()->Update();//anddrawfromunchangedoffscreenbitmapiGc->BitBlt(TPoint(0,0),&(iDoub1eBufferedArea->GetDoub1eBufferedAreaBitmap()));iClient.Flush();));1.5.声音处理我的引擎中使用CMdaAudioOutputStream和MMdaAudioOutputStreamCalIback完成声音播放功能。它主要有三个类组成:CAudioStreamPlayero它复合CMdaAudioOutputStream,继承CActive,实现MMdaAudioOutputStreamCallback接口。我们需要小心的维持缓冲区的大小以获得低延迟播放。CActive不断的建立新的

任务,在RunL函数中估算缓冲区中的剩余数据,向其中追加适当的数据,维持缓冲区的预期大小。CSimpleMixer0它实现CAudioGenerator接口。因为CMdaAudioOutputStream是ー个单ー的流式播放器,所以需要写ー个混音器进行波形混合。这里波形混合就是简单的数据相加。混音器有许多的声道(channel)〇每个channel记录了其中的CAudio指针和当前播放位置。CAudiOo包含ー个音频缓冲区。对每个声音文件,我们还需要一个类把它载入到内存缓冲区中。我不会在此讲解如何实现音频播放,那需要单独的ー篇文章。如果你也使用这种方法实现声音播放,我只想在此和大家讨论两个问题。需要学习声音基础的话,可以参考/article.php?id_article=l13。(可惜我当时学习声音时那篇文章和代码找不到了)1.5.1,声音的关闭和开启因为整个音频系统是ー个拉的结构,音频流从混音器那里拉数据,混音器从音频缓冲区中拉数据。所以,只要把CMdaAudioOutputStream和写数据的CActive对象delete掉,声音播放就全部停止了。在我的实现中,也就是deleteCAudioStreamPlayer对象即可。再想要开启声音,只需要重新创建

这个对象。这个实现的好处是程序的其它部分不需要保存声音是否开启这个状态。因为CAudio和CSimpleMixer对象是存在的,CAudio就可以把自己插入到Mixer的channel中,觉得自己好像在播放ー样。其实因为CAudioStreamPlayer根本没有从Mixer向外拉数据,声音设备是完全停止的。但是在恢复声音播放时有一点需要注意,恢复前需要清空混音器中的声音数据。因为经过了长时间的运行,混音器中的各个channel中已经塞满了各种声音。如果此时突然打开,会传出各种延迟了的杂音。1.5.2,特殊错误处理MMdaAudioOutputStreamCallback接口中的儿个回调函数MaoscOpenComplete>MaoscBufferCopied和MaoscPlayComplete都有一个错误码参数。你不能忽略这个参数。比如MaoscPlayComplete函数,是在音频停止播放时被调用。停止播放的原因可能是多种多样的。我们都知道要处理KErrUnderflow这个情况,这个错误吗意味着混音器没有及时的供给它音频数据。此时需要重新启动声音流。但是还有一些情况比如KErrDied和KErrlnUse很容易被忽略。KErrDied发生在接听电话时,此时声音线程已经死T,那么就需要重建整个音频系统。KErrlnUse发生在收到短信时,此时声音设备被抢占,用来播放短信提示音。此时你也需要重建整个

声音系统,但是此时不能立刻重建,否则还是ー样的结果。你应该等待几秒钟之后オ重建它。上面说的重启声音流和重建声音系统深度不同。重启声音流在稍后的代码中可以看到。其中RunAudioL向音频流写入了第一个声音缓冲区。重建声音系统在我的实现中就是指先delete再NewL创建CAudioStreamP1ayer对象。这三个错误的处理代码如下://AudiostreamAPIcallback:Calledwhenplaybackhasfinished,voidCAudioStreamPlaye

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论