如何使用动态链接库中的资源



近来在论坛上很有多帖子问到如何使用DLL中的资源(包括对话框,图标等)的问题,现在笔者就来就此问题谈谈,包含在DLL内部使用资源,DLL中使用其它DLL中的资源和在应用程序中使用资源。
我们先以图标为例说起(其它的资源与此图标的加载原理大致相同),我们要加载图标,一般是调用AfxGetApp()-> LoadIcon(…);下面是CWinApp::LoadIcon的实现(afxwin2.inl):
_AFXWIN_INLINE HICON CWinApp::LoadIcon(LPCTSTR lpszResourceName) const
{ return ::LoadIcon(AfxFindResourceHandle(lpszResourceName,
                    RT_GROUP_ICON), lpszResourceName); }
_AFXWIN_INLINE HICON CWinApp::LoadIcon(UINT nIDResource) const
{ return ::LoadIcon(AfxFindResourceHandle(MAKEINTRESOURCE(nIDResource),
                    RT_GROUP_ICON), MAKEINTRESOURCE(nIDResource)); }
可以看到CWinApp::LoadIcon实际上调用了API .LoadIcon,下面是API LoadIcon的原型:
HICON LoadIcon(

               HINSTANCE hInstance,
               LPCTSTR lpIconName
               );

hInstance
[in] Handle to an instance of the module whose executable file contains the icon to be loaded. This parameter must be NULL when a standard icon is being loaded.
hInstance是我们要加载ICON的模块实例,这个实例从何来,当然我们可以直接传入DLL的实例,对于通过LoadLibrary动态加载的DLL我们可以很容易的得到其句柄,但对于我们直接链接的DLL得到其句柄则要费一番周折。可以看到CWinApp::LoadIcon是通过AfxFindResouceHandle找到此句柄的。下面是AfxFindResourceHandle的定义(afxwin.h):
#ifndef _AFXDLL
#define AfxFindResourceHandle(lpszResource, lpszType) AfxGetResourceHandle()
#else
HINSTANCE AFXAPI AfxFindResourceHandle(LPCTSTR lpszName, LPCTSTR lpszType);
#endif
我们先讨论在静态链接库中使用MFC DLL的情况。可以看到,我们如果在静态库中使用MFC DLL的话(#ifndef _AFXDLL),实际上就是调用的AfxGetResourceHandle,MSDN中的说明是
AfxGetResourceHandle
This function accesses the application’s resources directly by using the HINSTANCE handle returned, for example, in calls to the Windows function FindResource.
HINSTANCE AfxGetResourceHandle( );
Return Value
An HINSTANCE handle where the default resources of the application are loaded.
函数返回的应用程序加载的缺省资源的HINSTANCE句柄,HINSTANCE相当于HMODULE,也就是资源所在的模块句柄。显然在此,我们使用的是DLL中的资源,那么我们就应该返回此DLL中的HINSTANCE了,如果让AfxGetResourceHandle返回DLL的HINSTANCE呢?答案是通过AfxSetResourceHandle设置。MSDN中AfxSetResouceHandle的说明如下:
AfxSetResourceHandle
This function sets the HINSTANCE handle that determines where the default resources of the application are loaded.
void AfxSetResourceHandle(
                          HINSTANCE hInstResource );
Parameters
hInstResource
Specifies the instance or module handle to an .EXE or DLL file from which the application’s resources are loaded.
我们只需将DLL的HINSTANCE传入AfxSetResouceHanle就行了。如何得到DLL的HINSTANCE呢,我们可以通过DLL声明一个接口HINSTANCE GetInstance获得,也可以通过EnumProcessMoudules找到(详细的过程见MSDN中EnumProcessModules的说明和示例)。
我们使用完DLL中的资源要使用EXE中的资源的资源怎么办呢?我们需要在使用完成后用AfxSetResource重新将资源模块的句柄设置为原来的值,如果来保证在资源使用完成后完成这一个工作呢,即使在使用过程中发生异常了,为此我们利C++类的构造和析构机制创建了这一类:
class CLocalResource
{
public:
   CLocalResource(HINSTANCE hInstance)
   {
      m_hInstOld=AfxGetInstanceHandle();
      AfxSetInstanceHandle(hInstance);
   }
   virtual ~CLocalResource()
   {
      AfxSetInstanceHandle(m_hInstOld);
   }
protected:
   HINSTANCE m_hInstOld;
};
我们只需在使用DLL的资源之前构造一个CLocalInstance就行了。
void CXXXX::LoadResouceFromDLL(HINSTANCE hInst,UINT nResID,…)
{
   CLocalResouce localRes(hInst);
   …
}
下面来讨论在动态库中使用MFC DLL的情况(也就是定义了_AFXDLL的情况)
来看看AfxGetInstanceHandle ,AfxGetResourceHandle和AfxSetResouceHandle的实现(afxwin1.inl中):
_AFXWIN_INLINE HINSTANCE AFXAPI AfxGetInstanceHandle()
{ ASSERT(afxCurrentInstanceHandle != NULL);
return afxCurrentInstanceHandle; }
_AFXWIN_INLINE HINSTANCE AFXAPI AfxGetResourceHandle()
{ ASSERT(afxCurrentResourceHandle != NULL);
return afxCurrentResourceHandle; }
_AFXWIN_INLINE void AFXAPI AfxSetResourceHandle(HINSTANCE hInstResource)
{ ASSERT(hInstResource != NULL); afxCurrentResourceHandle = hInstResource; }
实际上访问的就是afxCurrentInstanceHandle,afxCurrentResourceHandle。
/
// Global functions for access to the one and only CWinApp

#define afxCurrentInstanceHandle AfxGetModuleState()-> m_hCurrentInstanceHandle
#define afxCurrentResourceHandle AfxGetModuleState()-> m_hCurrentResourceHandle
….
AFX_MODULE_STATE* AFXAPI AfxGetModuleState()
{
   _AFX_THREAD_STATE* pState = _afxThreadState;
   AFX_MODULE_STATE* pResult;
   if (pState-> m_pModuleState != NULL)
   {
      // thread state 's module state serves as override
      pResult = pState-> m_pModuleState;
   }
   else
   {
      // otherwise, use global app state
      pResult = _afxBaseModuleState.GetData();
   }
   ASSERT(pResult != NULL);
   return pResult;
}
其中的_AFX_THREAD_STATE在此我们就不讨论了,有兴趣的读者可以阅读(afxstat_.h和afxstate.cpp)。
那AfxGetModuleState()-> m_hCurrentResourceHandle又是在哪初始化的呢?
我们可以在appinit.cpp中的BOOL AFXAPI AfxWinInit(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow)看到
// set resource handles
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
pModuleState-> m_hCurrentInstanceHandle = hInstance;
pModuleState-> m_hCurrentResourceHandle = hInstance;
和appinit.cpp中的void CWinApp::SetCurrentHandles()中看到
AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();
pModuleState-> m_hCurrentInstanceHandle = m_hInstance;
pModuleState-> m_hCurrentResourceHandle = m_hInstance;
CWinApp::SetCurrentHandles()也是由AfxWinInit调用的,那AfxWinInit又由谁调用呢?
我们可以在DllMain(dllinit.cpp和dllmodul.cpp,分别对应于MFC扩展DLL和MFC规则DLL)看到
(dllinit.cpp,MFC扩展DLL)
static AFX_EXTENSION_MODULE coreDLL;
….
extern "C "
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID)
{
   if (dwReason == DLL_PROCESS_ATTACH)
   {
      …….
         // initialize this DLL 's extension module
         VERIFY(AfxInitExtensionModule(coreDLL, hInstance));
#ifdef _AFX_OLE_IMPL
      AfxWinInit(hInstance, NULL, _T( " "), 0);

      ….
#endif
         ….
         // wire up this DLL into the resource chain
         CDynLinkLibrary* pDLL = new CDynLinkLibrary(coreDLL, TRUE);
      ASSERT(pDLL != NULL);
      pDLL-> m_factoryList.m_pHead = NULL;
      ….
   }
   else if (dwReason == DLL_PROCESS_DETACH)
   {
      ….
         // cleanup module state for this process
         AfxTermExtensionModule(coreDLL);
      ….
         // cleanup module state in OLE private module state
         AfxTermExtensionModule(coreDLL, TRUE);
      ….
   }
   …
}
可以看到在提供自动化支持时,将调用AfxWinInit(但MFC的DLL向导,对于扩展DLL却不允许添加自动化支持)。
(在dllmodul.cpp中,MFC常规DLL)
#ifdef _AFXDLL
static AFX_EXTENSION_MODULE controlDLL;
….
#endif

extern "C "
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID )
{
   if (dwReason == DLL_PROCESS_ATTACH)
   {
      ….
         _AFX_THREAD_STATE* pState = AfxGetThreadState();
      AFX_MODULE_STATE* pPrevModState = pState-> m_pPrevModuleState;

      // Initialize DLL 's instance(/module) not the app 's
      if (!AfxWinInit(hInstance, NULL, _T( " "), 0))
      {
         AfxWinTerm();
         goto Cleanup; // Init Failed
      }

      ….
#ifdef _AFXDLL
         // wire up this DLL into the resource chain
         VERIFY(AfxInitExtensionModule(controlDLL, hInstance));
      CDynLinkLibrary* pDLL; pDLL = new CDynLinkLibrary(controlDLL);
      ASSERT(pDLL != NULL);
#else
         AfxInitLocalData(hInstance);
#endif
      …
   }
   else if (dwReason == DLL_PROCESS_DETACH)
   {
      ….
#ifdef _AFXDLL
         AfxTermExtensionModule(controlDLL, TRUE);
#else
         AfxTermLocalData(hInstance, TRUE);
#endif
   }
   …
}
看到上面的代码,其实与MFC扩展DLL的代码大同小异,只过是MFC常规DLL可以支持在静态链接库全用MFC DLL,相应的初始化/反初始化函数就变为了AfxInitLocalData/ AfxTermLocalData,在此我们不对在静态链接库中使用MFC DLL的情况作更多讨论。
以及在winmain.cpp中的int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow),在appmodul.cpp中我们可以看到入口函数的实现:
extern "C " int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
          LPTSTR lpCmdLine, int nCmdShow)
{
   // call shared/exported WinMain
   return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}
看看AfxInitExtensionModule , AfxTermExtensionModule CDynLinkLibrary的定义(afxdll_.h)和实现(在dllinit.cpp中)就明白了DllMain做了什么:

BOOL AFXAPI AfxInitExtensionModule(AFX_EXTENSION_MODULE& state, HMODULE hModule)
{
   // only initialize once
   if (state.bInitialized)
   {
      AfxInitLocalData(hModule);
      return TRUE;
   }
   state.bInitialized = TRUE;
   // save the current HMODULE information for resource loading
   ASSERT(hModule != NULL);
   state.hModule = hModule;
   state.hResource = hModule;
   // save the start of the runtime class list
   AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
   state.pFirstSharedClass = pModuleState-> m_classList.GetHead();
   pModuleState-> m_classList.m_pHead = pModuleState-> m_pClassInit;

#ifndef _AFX_NO_OLE_SUPPORT
   // save the start of the class factory list
   state.pFirstSharedFactory = pModuleState-> m_factoryList.GetHead();
   pModuleState-> m_factoryList.m_pHead = pModuleState-> m_pFactoryInit;
#endif
   return TRUE;
}

void AFXAPI AfxTermExtensionModule(AFX_EXTENSION_MODULE& state, BOOL bAll)
{
   // make sure initialized
   if (!state.bInitialized)
      return;

   // search for CDynLinkLibrary matching state.hModule and delete it
   ASSERT(state.hModule != NULL);
   AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
   AfxLockGlobals(CRIT_DYNLINKLIST);
   for (CDynLinkLibrary* pDLL = pModuleState-> m_libraryList; pDLL != NULL; )
   {
      CDynLinkLibrary* pNextDLL = pDLL-> m_pNextDLL;
      if (bAll || pDLL-> m_hModule == state.hModule)
         delete pDLL; // will unwire itself
      pDLL = pNextDLL;
   }
   AfxUnlockGlobals(CRIT_DYNLINKLIST);
   // delete any local storage attached to this module
   AfxTermLocalData(state.hModule, TRUE);
   // remove any entries from the CWnd message map cache
   AfxResetMsgCache();
}

class CDynLinkLibrary : public CCmdTarget
{
   DECLARE_DYNAMIC(CDynLinkLibrary)
public:

   // Constructor
   explicit CDynLinkLibrary(AFX_EXTENSION_MODULE& state, BOOL bSystem = FALSE);
   CDynLinkLibrary(HINSTANCE hModule, HINSTANCE hResource);

   // Attributes
   HMODULE m_hModule;
   HMODULE m_hResource; // for shared resources
   ….
      BOOL m_bSystem; // TRUE only for MFC DLLs
   // Implementation
public:
   CDynLinkLibrary* m_pNextDLL; // simple singly linked list
   virtual ~CDynLinkLibrary();
   ….
};

CDynLinkLibrary::CDynLinkLibrary(AFX_EXTENSION_MODULE& state, BOOL bSystem)
{
   …
      // copy info from AFX_EXTENSION_MODULE struct
      ASSERT(state.hModule != NULL);
   m_hModule = state.hModule;
   m_hResource = state.hResource;
   m_classList.m_pHead = state.pFirstSharedClass;
   ….
      m_bSystem = bSystem;

   // insert at the head of the list (extensions will go in front of core DLL)
   DEBUG_ONLY(m_pNextDLL = NULL);
   AfxLockGlobals(CRIT_DYNLINKLIST);
   m_pModuleState-> m_libraryList.AddHead(this);
   AfxUnlockGlobals(CRIT_DYNLINKLIST);
}

CDynLinkLibrary::CDynLinkLibrary(HINSTANCE hModule, HINSTANCE hResource)
{
   …
      m_hModule = hModule;
   m_hResource = hResource;
   m_classList.m_pHead = NULL;
   …
      m_bSystem = FALSE;
   // insert at the head of the list (extensions will go in front of core DLL)
   DEBUG_ONLY(m_pNextDLL = NULL);
   AfxLockGlobals(CRIT_DYNLINKLIST);
   m_pModuleState-> m_libraryList.AddHead(this);
   AfxUnlockGlobals(CRIT_DYNLINKLIST);
}

CDynLinkLibrary::~CDynLinkLibrary()
{
   // remove this frame window from the list of frame windows
   AfxLockGlobals(CRIT_DYNLINKLIST);
   m_pModuleState-> m_libraryList.Remove(this);
   AfxUnlockGlobals(CRIT_DYNLINKLIST);
}
由此我们可以看出DLL初始化时先调AfxInitExtensionModule是为了构造一个AFX_EXTENSION_MODULE,然后将它插入插入一个AFX_MODULE_STATE链表中,同时将当前DLL的HINSTANCE插入此AFX_MODULE_STATE的CDynLinkLibrary链表中,以便稍后讲到的AfxFindResourceHandle找到。DLL从内存移出时就从此链表中删除自己。
让我们来看看AfxFindResourceHandle的实现(dllinit.cpp):
HINSTANCE AFXAPI AfxFindResourceHandle(LPCTSTR lpszName, LPCTSTR lpszType)
{
   ASSERT(lpszName != NULL);
   ASSERT(lpszType != NULL);

   HINSTANCE hInst;

   // first check the main module state
   AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
   if (!pModuleState-> m_bSystem)
   {
      hInst = AfxGetResourceHandle();
      if (::FindResource(hInst, lpszName, lpszType) != NULL)
         return hInst;
   }

   // check for non-system DLLs in proper order
   AfxLockGlobals(CRIT_DYNLINKLIST);
   CDynLinkLibrary* pDLL;
   for (pDLL = pModuleState-> m_libraryList; pDLL != NULL;
      pDLL = pDLL-> m_pNextDLL)
   {
      if (!pDLL-> m_bSystem && pDLL-> m_hResource != NULL &&
         ::FindResource(pDLL-> m_hResource, lpszName, lpszType) != NULL)
      {
         // found it in a DLL
         AfxUnlockGlobals(CRIT_DYNLINKLIST);
         return pDLL-> m_hResource;
      }
   }
   AfxUnlockGlobals(CRIT_DYNLINKLIST);

   // check language specific resource next
   hInst = pModuleState-> m_appLangDLL;
   if (hInst != NULL && ::FindResource(hInst, lpszName, lpszType) != NULL)
      return hInst;

   // check the main system module state
   if (pModuleState-> m_bSystem)
   {
      hInst = AfxGetResourceHandle();
      if (::FindResource(hInst, lpszName, lpszType) != NULL)
         return hInst;
   }

   // check for system DLLs in proper order
   AfxLockGlobals(CRIT_DYNLINKLIST);
   for (pDLL = pModuleState-> m_libraryList; pDLL != NULL; pDLL = pDLL-> m_pNextDLL)
   {
      if (pDLL-> m_bSystem && pDLL-> m_hResource != NULL &&
         ::FindResource(pDLL-> m_hResource, lpszName, lpszType) != NULL)
      {
         // found it in a DLL
         AfxUnlockGlobals(CRIT_DYNLINKLIST);
         return pDLL-> m_hResource;
      }
   }
   AfxUnlockGlobals(CRIT_DYNLINKLIST);

   // if failed to find resource, return application resource
   return AfxGetResourceHandle();
}
上面的代码首先检查主模块的状态,通过返回的是个AFX_MODULE_STATE的类对象指针. AFX_MODULE_STATE又是个什么呢?下面看看AFX_MODULE_STATE的定义(afxstat_.h中,它的实现在afxstate.cpp中,有兴趣的读者可以读读):
// AFX_MODULE_STATE (global data for a module)
class AFX_MODULE_STATE : public CNoTrackObject
{
public:
#ifdef _AFXDLL
   AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,
      BOOL bSystem = FALSE);
#else
   explicit AFX_MODULE_STATE(BOOL bDLL);
#endif
   ~AFX_MODULE_STATE();

   CWinApp* m_pCurrentWinApp;
   HINSTANCE m_hCurrentInstanceHandle;
   HINSTANCE m_hCurrentResourceHandle;
   LPCTSTR m_lpszCurrentAppName;
   BYTE m_bDLL; // TRUE if module is a DLL, FALSE if it is an EXE
   BYTE m_bSystem; // TRUE if module is a "system " module, FALSE if not
   …

#ifdef _AFXDLL
      // CDynLinkLibrary objects (for resource chain)
      CTypedSimpleList <CDynLinkLibrary*> m_libraryList;

   // special case for MFC71XXX.DLL (localized MFC resources)
   HINSTANCE m_appLangDLL;
#endif
   ….
};
我们来看看我们关心的属性
CWinApp* m_pCurrentWinApp;
HINSTANCE m_hCurrentInstanceHandle;
HINSTANCE m_hCurrentResourceHandle;
LPCTSTR m_lpszCurrentAppName;

BYTE m_bDLL; // TRUE if module is a DLL, FALSE if it is an EXE
BYTE m_bSystem; // TRUE if module is a "system " module, FALSE if not

// CDynLinkLibrary objects (for resource chain)
CTypedSimpleList <CDynLinkLibrary*> m_libraryList;

// special case for MFC71XXX.DLL (localized MFC resources)
HINSTANCE m_appLangDLL;
看了相关注释,也就明白什么意思了,在此就不多解释了。

如果当前主模块是系统模块(通过m_bSystem标识),并且从当前模块中找到了相应的资源就返回此模块的HINSTANCE。接下来当前模块所载的DLL链表中的所有用户模块中查找,本地化的MFC资源DLL中查找,系统DLL中查找。如果都找不到则从EXE(或者通过AfxSetResouceHandle中设置的HINSTANCE)中查找。
如果不考虑DLL链表,则需要我们每次使用DLL中的资源前调用AfxSetResouceHandle,使用后再调用AfxSetResourceHandle,就同前面讲的在静态链接库中使用MFC DLL一样。麻不麻烦?
怎么解决呢?解决方法是用资源DLL的HINSTANCE构造一CDynLinkLibrary对象插入主模块(EXE)的AFX_MODULE_STATE的m_libraryList中。怎么做?
看了上面DllMain的实现代码,我们可以照着做。
static AFX_EXTENSION_MODULE ResouceDLL = { NULL, NULL }
BOOL CXApp::InitInstance()
{
   // Get the resource DLL instance
   if (!AfxInitExtensionModule(ResouceDLL, hResourceModule))
      return 0;
   new CDynLinkLibrary(ResouceDLL);
   …
}
int CGenericMFCApp::ExitInstance()
{
   AfxTermExtensionModule(ResouceDLL);
   return CWinApp::ExitInstance();
}
这样我们就可以在应用程序中安全的使用DLL中的资源了,就好比EXE中的一样。对于我们自己编写入口函数的DLL,如果其中涉及到资源的话,我们可以参考上述的MFC常规DLL的实现。
对于我们在ActiveX和一些DLL中的AFX_MANAGE_STATE(AfxGetStaticModuleState());我们又怎么理解呢?看看afxstat_.h:
#ifdef _AFXDLL

class _AFX_THREAD_STATE;
struct AFX_MAINTAIN_STATE2
{
   explicit AFX_MAINTAIN_STATE2(AFX_MODULE_STATE* pModuleState);
   ~AFX_MAINTAIN_STATE2();
protected:
   AFX_MODULE_STATE* m_pPrevModuleState;
   _AFX_THREAD_STATE* m_pThreadState;
};
#define AFX_MANAGE_STATE(p) AFX_MAINTAIN_STATE2 _ctlState(p);
#else // _AFXDLL
#define AFX_MANAGE_STATE(p)
#endif //!_AFXDLL

对于在静态库中使用MFC DLL时它就相当于不存在,反之,它构造一个AFX_MAINTAIN_STATE2对象,在afxstate.cpp中
#ifdef _AFXDLL

AFX_MAINTAIN_STATE2::AFX_MAINTAIN_STATE2(AFX_MODULE_STATE* pNewState)
{
   m_pThreadState = _afxThreadState;
   m_pPrevModuleState = m_pThreadState-> m_pModuleState;
   m_pThreadState-> m_pModuleState = pNewState;
}
#endif //_AFXDLL
,
再看dllmodul.cpp中有关AfxGetStaticModuleState的实现:

static _AFX_DLL_MODULE_STATE afxModuleState;

AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState()
{
   AFX_MODULE_STATE* pModuleState = &afxModuleState;
   return pModuleState;
}

(MFC中太多的全局变量的确不雅!)
可以看到通过构造一个AFX_MAINTAIN_STATE2对象(再结合AfxGetResouceHandle的实现),就使AfxFindResourceHandle返回的是ActiveX控件(实际上也是一个常规DLL)或DLL的HINSTANE了,这样AfxFindResourceHandle就不必须在DLL链表中查找了,但缺点是每次使用资源(或响应消息)时都要构造一个AFX_MAINTAIN_STATE2对象,并且,而且使用这种方法在此DLL中无法使用其它DLL中的资源或在EXE中使用DLL中的资源(在不使用AfxSetResourceHandle/AfxGetResourceHandle的前提下)。当然,我们这儿关心的只是资源的使用,如果涉及到其它,包括事件响应或多个UI线程时,AFX_MODULE_STATE还是不能少的。
对于在静态库中使用MFC DLL的资源DLL,我们如果那有它的RES文件,那很好办,我们可以直接将.res像.lib链接到工程中就行了,上面的CLocalResource就可不用了,前提是有.res,有一些工具也可以完成这一工作(从DLL中或EXE中提取资源)!


对于在静态库中使用MFC DLL的资源DLL,我们如果那有它的RES文件,那很好办,我们可以直接将.res像.lib链接到工程中就行了,上面的CLocalResource就可不用了,前提是有.res,有一些工具也可以完成这一工作(从DLL中或EXE中提取资源)!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值