zoukankan      html  css  js  c++  java
  • cocos2d-x 的CCObject与autorelease 之深入分析

    转自: http://blog.csdn.net/honghaier/article/details/8160519

    CCObject.h:

    #ifndef __CCOBJECT_H__
    #define __CCOBJECT_H__
    
    #include "platform/CCPlatformMacros.h"
    //Cocos2d命名空间
    NS_CC_BEGIN
    
    //声明以下几种类,在后面会定义相应类的成员变量的指针。以下几个类均是CCObject类的派生类。
    class CCZone;  //CCObject指针暂存类
    class CCObject;//基类
    class CCNode;  //结点类
    class CCEvent; //事件类
    
    //这里定义了一个拷贝类
    class CC_DLL CCCopying
    {
    public:
        //虚函数。功能是为CCZone指针所指向的对象复制一份新的CCObject对象指针
        virtual CCObject* copyWithZone(CCZone* pZone);
    };
    //由CCCopying派生出CCObject
    class CC_DLL CCObject : public CCCopying
    {
    public:
        // 唯一ID
        unsigned int        m_uID;
        // 在LUA脚本引擎中的访问标识ID.暂可不理会,待学习到LUA时再分析
        int                 m_nLuaID;
    protected:
        // 引用计数器,进行内存计数。
        unsigned int        m_uReference;
        // 是否是被内存管理器进行托管而自动进行释放。
        bool                m_bManaged;        
    public:
        //构造函数
        CCObject(void);
        //析构函数
        virtual ~CCObject(void);
        //释放
        void release(void);
        //保留
        void retain(void);
        //设置实例对象的释放由内存管理器进行管理。实现自动释放。
        CCObject* autorelease(void);
        //取得一个拷贝
      CCObject* copy(void);
      //本类的实例化对象是否只有一个使用者
      bool isSingleRefrence(void);
      //返回内存计数器的值,也就是取得使用者的个数
        unsigned int retainCount(void);
        //判断是否与另一个CCObject实例对象相同
        virtual bool isEqual(const CCObject* pObject);
        //更新函数
        virtual void update(ccTime dt) {CC_UNUSED_PARAM(dt);};
        //设定CAutoreleasePool为友元类,这是一个通过CCObject指针容器CCMutableArray来对CCObject实例对象的内存进行管理的类,CCMutableArray在加入CCObject时对其引用计数器加1,在移除CCObject时对其引用计数器减1。
        friend class CCAutoreleasePool;
    };
    
    //定义一些函数
    //定义定时器访问类成员函数
    typedef void (CCObject::*SEL_SCHEDULE)(ccTime);
    //定义普通回调类成员函数
    typedef void (CCObject::*SEL_CallFunc)();
    //定义带结点参数的回调类成函数
    typedef void (CCObject::*SEL_CallFuncN)(CCNode*);
    //定义带结点参数及1个用户值参数的回调类成员函数
    typedef void (CCObject::*SEL_CallFuncND)(CCNode*, void*);
    typedef void (CCObject::*SEL_CallFuncO)(CCObject*);
    //定义菜单响应类成员函数
    typedef void (CCObject::*SEL_MenuHandler)(CCObject*);
    //定义事件响应类成员函数
    typedef void (CCObject::*SEL_EventHandler)(CCEvent*);
    //定义一些宏来取得这些回调成员函数的指针。
    #define schedule_selector(_SELECTOR) (SEL_SCHEDULE)(&_SELECTOR)
    #define callfunc_selector(_SELECTOR) (SEL_CallFunc)(&_SELECTOR)
    #define callfuncN_selector(_SELECTOR) (SEL_CallFuncN)(&_SELECTOR)
    #define callfuncND_selector(_SELECTOR) (SEL_CallFuncND)(&_SELECTOR)
    #define callfuncO_selector(_SELECTOR) (SEL_CallFuncO)(&_SELECTOR)
    #define menu_selector(_SELECTOR) (SEL_MenuHandler)(&_SELECTOR)
    #define event_selector(_SELECTOR) (SEL_EventHandler)(&_SELECTOR)
    #define compare_selector(_SELECTOR) (SEL_Compare)(&_SELECTOR)
    
    NC_CC_END
    #endif // __CCOBJECT_H_

    CCObject.cpp:

    #include "CCObject.h"
    //内存管理器头文件
    #include "CCAutoreleasePool.h"
    //Cocos2d-x定义的一些宏的头文件
    #include "ccMacros.h"
    //加入脚本支持
    #include "scripte_support/CCScriptSupport.h"
    
    NS_CC_BEGIN
    //虚函数。这里简单处理一下。需要进行重载
    CCObject* CCCopying::copyWithZone(CCZone *pZone)
    {
    CC_UNUSED_PARAM(pZone);
    //如果不进行重载,则会提示没有重载实现函数功能。
        CCAssert(0, "not implement");
        return 0;
    }
    //构造
    CCObject::CCObject(void)
    {
        //定义一个静态UINT类型变量做为实例对象计数器,此值只会增长,不会减少,保证唯一。
        static unsigned int uObjectCount = 0;
        //将计数器加1后赋值给唯一ID。
        //注意:所有由此CCObject类派生的子类也会拥有这个唯一的ID。它可以使我们通过唯一ID来获取相应的实例对象。
        m_uID = ++uObjectCount;
        //脚本ID
         m_nLuaID = 0;
    
        // 当类进行实例化时,将m_uReference设为1
        m_uReference = 1;
        //初始化时设实例化对象由用户进行内存管理。如果new出一个对象,需要自行delete。
        m_bManaged = false;
    }
    //析构
    CCObject::~CCObject(void)
    {
        如果内存是由内存管理器统一管理,则调用内存管理器实例对象的移除函数对自已的内存进行释放。
        if (m_bManaged)
        {
            CCPoolManager::getInstance()->removeObject(this);
        }
    
        //如果有使用到LUA脚本,调用脚本引擎的实例对象的移除函数将本身从脚本引擎的实例对象中移除。
        if (m_nLuaID)
        {
         CCScriptEngineManager::sharedManager()->getScriptEngine()->removeCCObjectByID(m_nLuaID);
        }
    }
    //返回一个本类实例化对象的拷贝
    CCObject* CCObject::copy()
    {
        return copyWithZone(0);
    }
    //供使用者外部调用的释放函数
    void CCObject::release(void)
    {
        //先确保计数器是大于0的数值,说明正常有效
        CCAssert(m_uReference > 0, "reference count should greater than 0");
        //计数器减1
        --m_uReference;
        //如果计数器减为0,释放本类实例化对象占用的内存
        if (m_uReference == 0)
        {
            delete this;
        }
    }
    //使用者外部调用的,使用一次时更新计数器。
    void CCObject::retain(void)
    {
        CCAssert(m_uReference > 0, "reference count should greater than 0");
    
        ++m_uReference;
    }
    //设置当前类的实例化对象的内存管理交给内存管理器去管理,不手动进行内存计数器的处理。
    CCObject* CCObject::autorelease(void)
    {
        //调用内存管理器实例对象的addObject函数加入当前CCObject实例对象的指针
        CCPoolManager::getInstance()->addObject(this);
        //打开使用内存管理器的标记
        m_bManaged = true;
        return this;
    }
    //是否当前类的实例化对象只被一个使用者使用
    bool CCObject::isSingleRefrence(void)
    {
        //计数器的个数即代表使用者的个数,因为一个使用者使用一次,计数器加1
        return m_uReference == 1;
    }
    //返回当前类的实例化对象的使用者的个数
    unsigned int CCObject::retainCount(void)
    {
        return m_uReference;
    }
    //是否与另一个基类为CCObject的实例化对象是同一个对象
    bool CCObject::isEqual(const CCObject *pObject)
    {
        return this == pObject;
    }
    
    NS_CC_END 

    原理说明:

    我们看到CCObject其实真的很单纯,它主要就是有两个功能。一个是通过引用计数交给内存管理器进行内存管理。另一个就是通过脚本ID访问相应的脚本。脚本的分析后面专门有一章进行分析探讨,我们暂时只把内存管理的事情搞明白。好,下面我们来重点看一下autorelease函数的意义,顾名思义,“自动释放”。也就是说调用此函数则当前CCObject实例对象不需要用户在外部去手动调用release进行内存的释放工作。我们已经知道它通过引用计数来处理在什么时候内存释放。Cocos2d-x是怎么做到的呢?

    在autorelease函数中有这么一句

    CCPoolManager::getInstance()->addObject(this);

    CCPoolManager代表了内存管理器。此句调用CCPoolManager的实例对象的addObject函数将当前CCObject实例对象的指针交给内存管理器。我们来分析一下内存管理器的原理。

     

    读前小提示:CCMutableArray是一个CCObject指针容器类,它内部通过使用STL的vector容器来存储CCObject指针。在加入一个新CCObject时对其引用计数器加1,在移除CCObject时对其引用计数器减1。请各位同学自行打开CCMutableArray.h及cpp文件进行查看。

    重点函数:CCAutoreleasePool::release(),CCPoolManager::finalize()

    CCAutoreleasePool.h:

    #ifndef __AUTORELEASEPOOL_H__
    #define __AUTORELEASEPOOL_H__
    
    #include "CCObject.h"
    //加入CCObject指针容器类
    #include "CCArray.h"
    
    NS_CC_BEGIN
    //这里定义一个自动释放内存结点类。由CCObject派生,这个结点类通过其成员容器对CCObject实例对象指针进行内存的释放操作。
    class CC_DLL CCAutoreleasePool : public CCObject
    {
        // CCArray是一个CCObject指针容器类。对CCOjbect的实例对象指针进行管理。
        CCArray*    m_pManagedObjectArray;    
    public:
        //构造
        CCAutoreleasePool(void);
        //析构
        ~CCAutoreleasePool(void);
        //加入一个新的CCObject实例对象指针到容器
        void addObject(CCObject *pObject);
        //从容器中移除一个指定的实例对象CCObject指针
        void removeObject(CCObject *pObject);
        //清空容器
        void clear();
    };
    //定义类内存管理器,这个类通过其成员容器对上面定义的自动释放内存结点类实例对象指针进行管理。
    class CC_DLL CCPoolManager
    {    
        // 使用CCArray对要释放的CCAutoreleasePool实例对象指针进行管理
        CCArray* m_pReleasePoolStack;    
        //当前的CCAutoreleasePool实例对象指针
        CCAutoreleasePool* m_pCurReleasePool;
        //取得当前的CCAutoreleasePool实例对象指针
        CCAutoreleasePool* getCurReleasePool();
    public:
        //构造函数
        CCPoolManager();
        //析构函数
        ~CCPoolManager();
        //终结内存管理
        void finalize();
        //新建一个CCAutoreleasePool实例对象,将其指针以压栈方式存放到pReleasePoolStack中
        void push();
        //以出栈方式释放当前CCAutoreleasePool实例对象中管理的所有CCObject实例对象内存
        void pop();
        //从当前CCAutoreleasePool实例对象中移除一个指定的CCObject实例对象指针
        void removeObject(CCObject* pObject);
        //加入一个新的CCObject实例对象指针到CCAutoreleasePool实例对象中的容器
        void addObject(CCObject* pObject);
        //取得单件内存管理器实例对象指针
        static CCPoolManager* sharedPoolManager();
        //销毁单件内存管理器实例对象指针
        static void purgePoolManager();
        //声明CCAutoreleasePool是友元类
        friend class CCAutoreleasePool;
    };
    
    NS_CC_END
    
    #endif //__AUTORELEASEPOOL_H__

    CCAutoreleasePool.cpp:

    #include "CCAutoreleasePool.h"
    #include "ccMacros.h"
    
    NS_CC_BEGIN
    //定义全局唯一的内存管理器对象
    static    CCPoolManager*    s_pPoolManager = NULL;
    //构造函数
    CCAutoreleasePool::CCAutoreleasePool(void)
    {    //新建一个CCObject指针容器类实例对象
        m_pManagedObjectArray = new CCArray();
        m_pManagedObjectArray->init();
    }
    //析构
    CCAutoreleasePool::~CCAutoreleasePool(void)
    {   //删除m_pManagedObjectArray
        CC_SAFE_DELETE(m_pManagedObjectArray);
    }
    //加入一个新的CCObject指针到容器
    void CCAutoreleasePool::addObject(CCObject* pObject)
    {
        //调用CCObject指针容器对象m_pManagedObjectArray的addObject函数将pObject放入容器,注意:调用后pObject的引用计数器加1。因为它被新的使用者m_pManagedObjectArray暂存了指向内存的指针。
       m_pManagedObjectArray->addObject(pObject);
        //因为CCAutoreleasePool是CCObject的友元类,故可以直接访问CCObject的m_uReference变量,m_uReference代表了使用计数器,只有当计数器为0时才能释放,这里判断pObject->m_uReference是否大于1,否则弹出错误提示,因为CCObject类实例化时会引用计数值加1,上 一句函数又会使CCObject类实例化对象引用计数加1,所以到这里引用计数至少为2。
        CCAssert(pObject->m_uReference > 1, "reference count should greager than 1");
        //注意:调用释放函数进行引用计数减1操作。大家一定很困惑。这刚刚加1现在就减1是为了啥呀?其实这是内存管理器的非常关键的一句代码。因为我们前面说了,CCObject实例对象在调用其成员函数autorelease的目的是实现用户不需要考虑什么时候手动调用release进行内存释放。而调用CCObject指针容器对象m_pManagedObjectArray的addObject函数将pObject放入容器时,pObject等于被m_pManagedObjectArray所使用。其引用计数器加1.而在后面当pObject被从m_pManagedObjectArray中移除时,m_pManagedObjectArray会对其引用计数器减1,但因为用户不再手动进行release的调用,则引用计数器将始终为1导致其new出来的内存无法得到正确的释放。在这里做一次release对pObject的引用计数减1后,将当pObject被从m_pManagedObjectArray中移除时,m_pManagedObjectArray对其引用计数器减1,此时pObject的引用计数值为0,则会正确释放new出来的内存。当然,如果你对于这个函数的功能含义理解透了,也可以做一些小修改。比如将CCMutableArray的addObject函数中的引用计数加1操作去掉,这里的减1操作也去掉。这样CCMutableArray的身份就只是个存放和释放CCObject的功能容器。没有了使用者的身份,但提高了少许效率。
        pObject->release();  
    }
    //从容器中移除一个指定的CCObject指针
    void CCAutoreleasePool::removeObject(CCObject* pObject)
    {
        //调用指针容器模版类m_pManagedObjectArray的removeObject函数将pObject从容器中移除,这个函数只是移除,不对pObject的引用计数作减1操作。
        m_pManagedObjectArray->removeObject(pObject, false);
    }
    //清空容器中所有的内存节点并释放这些节点的内存
    void CCAutoreleasePool::clear()
    {
        //如果容器的元素数量不为空
        if(m_pManagedObjectArray->count() > 0)
        {
            //CCAutoreleasePool* pReleasePool;
    #ifdef _DEBUG
            int nIndex = m_pManagedObjectArray->count() - 1;
    #endif
            //反向遍历容器
            CCObject* pObj = NULL;
            CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
            {
                //如果指针为空则退出遍历
                if(!pObj)
                    break;
                //设其不再由管理器进行管理
                pObj ->m_bManaged = false;
                //注意:以下两行作者在引擎中注释了,是因为在后面removeAllObjects函数会遍历调用CCObject实例对象指针的release函数,而CCObject的release函数内部会进行自已的delete.
                //(*it)->release();
                //delete (*it);
    #ifdef _DEBUG
                nIndex--;
    #endif
            }
            //在调用容器的removeAllObjects函数时会遍历容器所有节点并调用其release函数。具体可以参看CCArray.h中对于CCArray类的具体函数实现。
            m_pManagedObjectArray->removeAllObjects();
        }
    }
    
    //获取内存管理器的单件实例对象的指针
    CCPoolManager* CCPoolManager::sharedPoolManager()
    {
        if (s_pPoolManager == NULL)
        {
            s_pPoolManager = new CCPoolManager();
        }
        return s_pPoolManager;
    }
    //销毁内存管理器的单件实例对象的指针
    void CCPoolManager::purgePoolManager()
    {
        CC_SAFE_DELETE(s_pPoolManager);
    }
    
    //构造函数
    CCPoolManager::CCPoolManager()
    {    
        //创建一个CCArray来存放CCObject的派生类CCAutoreleasePool的实例对象指针
        m_pReleasePoolStack = new CCArray ();    
        m_pReleasePoolStack->init();
         m_pCurReleasePool = 0;
    }
    //析构
    CCPoolManager::~CCPoolManager()
    {
        //终结处理函数,马上后面会讲
        finalize();
    
        //将m_pCurReleasePool设为空
         m_pCurReleasePool = 0;
        //这里将容器中第一个元素释放掉,为什么不调用removeAllObjects函数呢?[伏笔3.1.1]
        m_pReleasePoolStack->removeObjectAtIndex(0);
        //删除new所申请的容器内存
        CC_SAFE_DELETE(m_pReleasePoolStack);
    }
    //内存管理器终结函数。
    void CCPoolManager::finalize()
    {
        //如果容器不为空
        if(m_pReleasePoolStack->count() > 0)
        {
            //遍历容器中每个元素
            CCObject* pObj = NULL;
             CCARRAY_FOREACH(m_pReleasePoolStack, pObj) 
            {
                if(!pObj)
                    break;
                //将CCObject指针转为CCAutoreleasePool指针存入临时变量pPool。
                CCAutoreleasePool* pPool = (CCAutoreleasePool*)pObj;
                //调用其clear函数
                pObj ->clear();
            }
        }
    }
    //新建一个CCAutoreleasePool实例对象,将其指针以压栈方式存放到pReleasePoolStack中
    void CCPoolManager::push()
    {
        //新建CCAutoreleasePool实例对象,因CCAutoreleasePool为CCObject派生类,所以调用其基类构造时计数器为1
        CCAutoreleasePool* pPool = new CCAutoreleasePool();               //ref = 1
        m_pCurReleasePool = pPool;
        //将新建的CCAutoreleasePool实例对象放入容器中。因容器的addObject调用实例对象的retain函数,所以计数器变为2
        m_pReleasePoolStack->addObject(pPool);                   //ref = 2
        //注意:这里释放一下的目的是让计数器做一次减1操作。这样在对容器进行removeObject操作以及在容器clear时,元素不会因为addObject时对元素计数器的加1操作导致计数器值为1而无法delete,当然,你也可以在容器的removeObject和clear函数中对元素调用一次release来实现相同的目的。
        pPool->release();                                       //ref = 1
    }
    //以出栈方式释放当前CCAutoreleasePool实例对象中管理的所有CCObject实例对象内存
    void CCPoolManager::pop()
    {
        //如果当前没有CCAutoreleasePool实例对象了,就直接返回
        if (! m_pCurReleasePool)
        {
            return;
        }
        //取得容器中有几个CCAutoreleasePool实例对象
         int nCount = m_pReleasePoolStack->count();
        //清空一下当前使用的CCAutoreleasePool实例对象
        m_pCurReleasePool->clear();
         //如果容器的CCAutoreleasePool实例对象数量大于1,则释放第nCount-1个CCAutoreleasePool实例对象,在这里硬性的规定了外部调用pop函数只能释放容器中第nCount-1个CCAutoreleasePool实例对象,而将第一个CCAutoreleasePool实例对象在析构函数中进行释放,这就解答了伏笔3.1.1,而且Cocos2d-x并没有在这里直接遍历所有CCAutoreleasePool实例对象进行释放,它在当前只释放了第nCount-1个CCAutoreleasePool实例对象是怎么回事呢?[伏笔3.1.2]
          if(nCount > 1)
          {
            m_pReleasePoolStack->removeObjectAtIndex(nCount-1);
    
    //         if(nCount > 1)
    //         {
    //             m_pCurReleasePool = m_pReleasePoolStack->getObjectAtIndex(nCount - 2);
    //             return;
    //         }
            //将m_pCurReleasePool指向第nCount-2个元素
            m_pCurReleasePool = m_pReleasePoolStack->getObjectAtIndex(nCount - 2);
        }
    
        /*m_pCurReleasePool = NULL;*/
    }
    //从当前CCAutoreleasePool实例对象中移除一个指定的CCObject实例对象指针
    void CCPoolManager::removeObject(CCObject* pObject)
    {
        //先判断一下m_pCurReleasePool是否有效
        CCAssert(m_pCurReleasePool, "current auto release pool should not be null");
        m_pCurReleasePool->removeObject(pObject);
    }
    //加入一个新的CCObject实例对象指针到CCAutoreleasePool实例对象中的容器
    void CCPoolManager::addObject(CCObject* pObject)
    {
        //注意:这里与removeObject对比,想想为什么不直接用m_pCurReleasePool?那是因为总是先调用addObject之后才能调用removeObject,故addObject可能存在m_pCurReleasePool为空的情况,而removeObject时不应该存在m_pCurReleasePool为空的情况。getCurReleasePool将会判断m_pCurReleasePool是否为空,如果为空则进行相关处理。
        getCurReleasePool()->addObject(pObject);
    }
    //取得m_pCurReleasePool
    CCAutoreleasePool* CCPoolManager::getCurReleasePool()
    {
        //如果m_pCurReleasePool为空则调用push
        if(!m_pCurReleasePool)
        {
            push();
        }
        //如果m_pCurReleasePool为空提示出错。
        CCAssert(m_pCurReleasePool, "current auto release pool should not be null");
        //返回m_pCurReleasePool
        return m_pCurReleasePool;
    }
    
    NS_CC_END

    虽然这个cpp讲解完了,但是可能有的朋友仍然会感觉身在云雾中一样。我再总结一下:Cocos2d-x提供了一个内存管理器类CCPoolManager,它有一个容器m_pReleasePoolStack,而这个容器是用来存放了一些容器管理类CCAutoreleasePool的实例对象的。需要自动进行内存释放的CCObject实例对象会把其指针存放在容器管理类CCAutoreleasePool的实例对象中的m_pManagedObjectArray容器里。所有存在其中的CCObject实例对象在进行释放操作时通过使用计数器来进行判断在何时真正delete内存。

                 

    现在我们来实际操作一下。我们在VC的代码查询里输入“CCPoolManager:: sharedPoolManager ()”然后回车。在查找结果里会看到:

    查找全部"CCPoolManager::sharedPoolManager()", 大小写匹配, 子文件夹, 查找结果1,"整个解决方案"

      C:cocos2d-2.0-x-2.0.2cocos2dxcocoaCCAutoreleasePool.cpp(90):CCPoolManager*CCPoolManager::sharedPoolManager()

      C:cocos2d-2.0-x-2.0.2cocos2dxcocoaCCObject.cpp(58):       CCPoolManager::sharedPoolManager()->removeObject(this);

      C:cocos2d-2.0-x-2.0.2cocos2dxcocoaCCObject.cpp(93):   CCPoolManager::sharedPoolManager()->addObject(this);

      C:cocos2d-2.0-x-2.0.2cocos2dxCCDirector.cpp(158):   CCPoolManager::sharedPoolManager()->push();

      C:cocos2d-2.0-x-2.0.2cocos2dxCCDirector.cpp(181):   CCPoolManager::sharedPoolManager()->pop();

      C:cocos2d-2.0-x-2.0.2cocos2dxCCDirector.cpp(971):         CCPoolManager::sharedPoolManager()->pop();       

    第一个结果是函数定义,略过。

    第二个结果是CCObject的析构,这里的意义就是如果CCObject的实例对象调用过autorelease将其内存管理交给内存管理器,则在析构时会调用内存管理器对其进行释放。

    第三个结果是CCObject的autorelease函数。这里的意义是将CCObject的实例对象指针通过参数传给内存管理器,交由内存管理器对其内存进行管理。

    第四个结果是在CCDirector的init函数中。这里的意义是在显示设备初始化时调用内存管理器的push函数新创建一个内存管理结点用来对后面的CCObject进行内存管理。

    第五个结果是在CCDirector的析构函数中。这里的意义是在最终结束游戏时调用内存管理器的pop函数对其管理的当前内存管理结点进行清空。

    第六个结果是在显示设备的每一帧渲染处理时调用内存管理器的pop函数对其管理的当前内存管理结点进行清空,看到了吧,每一帧进行调用,所以说即使它每一次只能释放容器中第nCount-1个CCAutoreleasePool实例对象。但它因为每帧都调用,所以始终可以保持其内部只有1个CCAutoreleasePool实例对象。在这里解答了伏笔3.1.1伏笔3.1.2。当然,如果你在伏笔3.1.1处将

    m_pReleasePoolStack->removeObjectAtIndex(0);

    改为:

    m_pReleasePoolStack->removeAllObjects();

    更好理解一些。

  • 相关阅读:
    C#LPT端口连接热敏打印机发送指令
    c# 普通打印机大致有三种方法(非热敏打印机及lpt1并口指令控制型)
    C#直接发送打印机命令到打印机(这里测试的是直接弹出钱箱操作)
    c#操作access,update语句不执行的解决办法
    element-ui dialog组件添加可拖拽位置 可拖拽宽高[转]
    [JavaScript] js实现简单的代码运行框【转】
    HTML5 drag & drop 拖拽与拖放简介[转]
    webpack 单独打包指定JS文件(转)
    跳转地图并定位
    基于Cesium实现逼真的水特效[转]
  • 原文地址:https://www.cnblogs.com/sevenyuan/p/3165011.html
Copyright © 2011-2022 走看看