iOS自动释放池AutoreleasePool

介绍iOS的自动释放池原理,把底层的方法分析一遍,并给每个方法都添加了注释

我们一般使用自动释放池是直接使用@autoreleasePool方法,如下
底层会帮我们再次翻译成另一种形式

int main(int argc, const char * argv[]) {
          @autoreleasePool{

          }
         return 0;
}

底层会翻译成这样
创建一个局部变量接收push函数的返回值,查看objc_autoreleasePoolPush和objc_autoreleasePoolPop方法,会发现内部都调用了AutoreleasePoolPage,

//这个是翻译后的自动释放池结构体
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
     __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
     ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); }
     void * atautoreleasepoolobj;
};
{ __AtAutoreleasePool __autoreleasepool;}
//创建释放池
void * objc_autoreleasePoolPush(void) {
               return AutoreleasePoolPage::push();
     }
//销毁释放池
void objc_autoreleasePoolPop(void *ctxt) {
            AutoreleasePoolPage::pop(ctxt);
     }
//main方法,翻译后的是这样
int main(int argc, const char * argv[]) {
          { 
          //atautoreleasepoolobj是哨兵对象
          void * atautoreleasepoolobj = objc_autoreleasePoolPush();  
                     
                objc_autoreleasePoolPop(atautoreleasepoolobj);
         }
         return 0;
 }

AutoreleasePoolPage的具体实现是这样的


struct magic_t {
    static const uint32_t M0 = 0xA1A1A1A1;
#   define M1 "AUTORELEASE!"
    static const size_t M1_len = 12;
    uint32_t m[4];
    
    magic_t() {
        assert(M1_len == strlen(M1));
        assert(M1_len == 3 * sizeof(m[1]));

        m[0] = M0;
        strncpy((char *)&m[1], M1, M1_len);
    }

    ~magic_t() {
        m[0] = m[1] = m[2] = m[3] = 0;
    }

    bool check() const {
        return (m[0] == M0 && 0 == strncmp((char *)&m[1], M1, M1_len));
    }

    bool fastcheck() const {
#if CHECK_AUTORELEASEPOOL
        return check();
#else
        return (m[0] == M0);
#endif
    }

#   undef M1
};
    /*
     @autoreleasepool{}
     extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
     extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
     struct __AtAutoreleasePool {
     __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
     ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); }
     void * atautoreleasepoolobj;
     };
     { __AtAutoreleasePool __autoreleasepool;}
     
     
     void * objc_autoreleasePoolPush(void) {

               return AutoreleasePoolPage::push();

     }

     void objc_autoreleasePoolPop(void *ctxt) {

            AutoreleasePoolPage::pop(ctxt);

     }
     
     int main(int argc, const char * argv[]) {

          { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want             objc_autoreleasePoolPop(atautoreleasepoolobj);

         }

         return 0;

     }
     */
//自动释放池,每页4096个字节,双向链表结构
//自动释放池的对象成员占用56个字节,所以添加的对象是从第56个字节开始存储到释放池中
class AutoreleasePoolPage 
{
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
//分界对象 (以前叫POOL_SENTINEL(哨兵对象)总之是一样的作用)
//在每个自动释放池初始化调用objc_autoreleasePoolPush的时候,都会把一个POOL_BOUNDARY push 到自动释放池的栈顶,并且返回这个POOL_BOUNDARY边界对象。
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;//指向下一个要添加的自动释放对象的位置
    pthread_t const thread;
    AutoreleasePoolPage * const parent;//指向上一页释放池
    AutoreleasePoolPage *child;//指向下一页释放池
    uint32_t const depth;
    uint32_t hiwat;

    // SIZE-sizeof(*this) bytes of contents follow

    //创建一块内存,大小4096个字节
    static void * operator new(size_t size) {
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    //释放内存
    static void operator delete(void * p) {
        return free(p);
    }

    //加锁
    inline void protect() {
#if PROTECT_AUTORELEASEPOOL
        mprotect(this, SIZE, PROT_READ);
        check();
#endif
    }
//解锁
    inline void unprotect() {
#if PROTECT_AUTORELEASEPOOL
        check();
        mprotect(this, SIZE, PROT_READ | PROT_WRITE);
#endif
    }

    //创建一个释放池,参数是上一个已经full满的释放池
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
        : magic(), next(begin()), thread(pthread_self()),
          parent(newParent), child(nil), 
          depth(parent ? 1+parent->depth : 0), 
          hiwat(parent ? parent->hiwat : 0)
    {
        //如果parent有值,说明上一个释放池已经满了
        if (parent) {
            parent->check();
            assert(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

    ~AutoreleasePoolPage() 
    {
        check();
        unprotect();
        assert(empty());

        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        assert(!child);
    }

//显示异常信息
    void busted(bool die = true) 
    {
        magic_t right;
        (die ? _objc_fatal : _objc_inform)
            ("autorelease pool page %p corrupted\n"
             "  magic     0x%08x 0x%08x 0x%08x 0x%08x\n"
             "  should be 0x%08x 0x%08x 0x%08x 0x%08x\n"
             "  pthread   %p\n"
             "  should be %p\n", 
             this, 
             magic.m[0], magic.m[1], magic.m[2], magic.m[3], 
             right.m[0], right.m[1], right.m[2], right.m[3], 
             this->thread, pthread_self());
    }
    //调用的方法check()用来检查当前的result是不是一个AutoreleasePoolPage,通过检查magic_t结构体中的某个成员是否为0xA1A1A1A1
    void check(bool die = true) 
    {
        if (!magic.check() || !pthread_equal(thread, pthread_self())) {
            busted(die);
        }
    }
//调用的方法fastCheck()用来检查当前的result是不是一个AutoreleasePoolPage,通过检查magic_t结构体中的某个成员是否为0xA1A1A1A1
    void fastcheck(bool die = true) 
    {
#if CHECK_AUTORELEASEPOOL
        check(die);
#else
        if (! magic.fastcheck()) {
            busted(die);
        }
#endif
    }

//起始位置
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));//sizeof(*this) = 56个字节
    }
//棧顶位置
    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

    //如果next指针等于begin,说明没有要释放的对象
    bool empty() {
        return next == begin();
    }

    //如果next指针等于end,说明满了
    bool full() { 
        return next == end();
    }

    //释放池中的数量是不是小于释放池容量的一半
    bool lessThanHalfFull() {
        return (next - begin() < (end() - begin()) / 2);
    }

    //把对象添加到自动释放池中,并返回对象所在的自动释放池中的位置的指针地址
    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

    //释放所有对象
    void releaseAll() 
    {
        releaseUntil(begin());
    }

    //释放到stop所指定的指针地址这个位置的所有对象
    void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release
            // autoreleased more objects
            //每次都从hotpage中获取对象去释放,防止释放过多
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            //如果这个释放池没有一个对象,就去释放他的上一个释放池
            while (page->empty()) {
                page = page->parent;
                //更换最新释放页,把当前页面设置为最新释放页
                setHotPage(page);
            }

            page->unprotect();
            //这句话可以这样看*(--(page->next)),就是先获取游标的地址,减减(--)是获取最后一个添加的对象所在的地址,因为next总是指向下一个空地址,
            //*是取地址所在的值
            id obj = *--page->next;
            //把next的位置设置为0xA3,标记为,以下都是要释放的对象
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            //如果对象不为nil,就释放对象
            if (obj != POOL_BOUNDARY) {
                //调释放函数
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

    //销毁所有的释放池
    void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        //通过循环,找到最新的一个释放池
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;//释放内存
        } while (deathptr != this);
    }
//清空释放池
    static void tls_dealloc(void *p) 
    {
        if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
            // No objects or pool pages to clean up here.
            return;
        }

        // reinstate TLS value while we work
        setHotPage((AutoreleasePoolPage *)p);

        if (AutoreleasePoolPage *page = coldPage()) {
            if (!page->empty()) pop(page->begin());  // pop all of the pools
            if (DebugMissingPools || DebugPoolAllocation) {
                // pop() killed the pages already
            } else {
                page->kill();  // free all of the pages
            }
        }
        
        // clear TLS value so TLS destruction doesn't loop
        setHotPage(nil);
    }

    //根据哨兵对象的指针地址获取当前页的释放池的首地址,一般传入的参数是哨兵对象
    static AutoreleasePoolPage *pageForPointer(const void *p) 
    {
        return pageForPointer((uintptr_t)p);
    }
    //根据哨兵对象的指针地址获取当前页的释放池的首地址,一般传入的参数是哨兵对象
    static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;

        assert(offset >= sizeof(AutoreleasePoolPage));

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }


    static inline bool haveEmptyPoolPlaceholder()
    {
        id *tls = (id *)tls_get_direct(key);
        return (tls == EMPTY_POOL_PLACEHOLDER);
    }

    static inline id* setEmptyPoolPlaceholder()
    {
        assert(tls_get_direct(key) == nil);
        tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
        return EMPTY_POOL_PLACEHOLDER;
    }
//hotPage可以理解为当前正在使用的AutoreleasePoolPage
    static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
//hotPage可以理解为当前正在使用的AutoreleasePoolPage
    static inline void setHotPage(AutoreleasePoolPage *page) 
    {
        if (page) page->fastcheck();
        tls_set_direct(key, (void *)page);
    }

    //获取第一个释放池
    static inline AutoreleasePoolPage *coldPage() 
    {
        AutoreleasePoolPage *result = hotPage();
        if (result) {
            while (result->parent) {
                result = result->parent;
                result->fastcheck();
            }
        }
        return result;
    }

//快速添加对象到释放池方法
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            //有hotPage并且当前page不满,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
            return page->add(obj);
        } else if (page) {
            //有hotPage并且当前page已满,调用autoreleaseFullPage初始化一个新的页,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
            return autoreleaseFullPage(obj, page);
        } else {
            //无hotPage,调用autoreleaseNoPage创建一个hotPage,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中,最后的都会调用page->add(obj)将对象添加到自动释放池中。
            return autoreleaseNoPage(obj);
        }
    }

    //当前的释放池满的时候,会调用这个方法,创建新的释放池,并添加对象
    static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }
无hotPage,调用autoreleaseNoPage创建一个hotPage,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中,最后的都会调用page->add(obj)将对象添加到自动释放池中。
    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        // "No page" could mean no pool has been pushed
        // or an empty placeholder pool has been pushed and has no contents yet
        assert(!hotPage());

        bool pushExtraBoundary = false;
        //判断是否需要插入哨兵
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            //报出错误
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            // 我们正在推送一个没有池的对象,
            // 并且环境请求了无池调试。
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            //报出错误
            // 我们正在推送一个没有池子的池子,
                         // 并且没有请求 alloc-per-pool 调试。
                         // 安装并返回空池占位符。
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            //如果obj是个nil对象,就创建一个空释放池
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.
        //创建一个空释放池
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        //设置为当前释放池
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
//            是否需要添加哨兵
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        //添加释放对象
        return page->add(obj);
    }


    static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

public:
    //把对象添加到自动释放池中
    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);//调用快速添加到自动释放池方法,把对象加到释放池中
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

//当我是用@autoreleasepool{},内部会调用这个静态方法去实现一个释放池对象,
    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            //dest这个就创建释放池成功后返回的哨兵对象,
            //后面调用pop方法释放的时候,会传入这个dest对象
            //这里是快速创建一个释放池,并添加一个哨兵对象
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;//把哨兵对象返回
    }
//释放出现异常,调用这个
    static void badPop(void *token)
    {
        // Error. For bincompat purposes this is not 
        // fatal in executables built with old SDKs.

        if (DebugPoolAllocation || sdkIsAtLeast(10_12, 10_0, 10_0, 3_0, 2_0)) {
            // OBJC_DEBUG_POOL_ALLOCATION or new SDK. Bad pop is fatal.
            _objc_fatal
                ("Invalid or prematurely-freed autorelease pool %p.", token);
        }

        // Old SDK. Bad pop is warned once.
        static bool complained = false;
        if (!complained) {
            complained = true;
            _objc_inform_now_and_on_crash
                ("Invalid or prematurely-freed autorelease pool %p. "
                 "Set a breakpoint on objc_autoreleasePoolInvalid to debug. "
                 "Proceeding anyway because the app is old "
                 "(SDK version " SDK_FORMAT "). Memory errors are likely.",
                     token, FORMAT_SDK(sdkVersion()));
        }
        objc_autoreleasePoolInvalid(token);
    }
    //释放对象方法,参数是push方法返回的哨兵对象
    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;
//这里是如果传入的token不是哨兵
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                //这里是获取第一个释放池的哨兵对象,开始释放
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);//如果没用过释放池,直接清空
            }
            return;
        }

        page = pageForPointer(token);//获取哨兵对象所在的释放池页面
        stop = (id *)token;
        //如果token不是哨兵对象
        if (*stop != POOL_BOUNDARY) {
            //如果stop等于这个释放页开始位置,并且释放页没有上一页,也就是这个释放页是第一页,不做处理
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                //否则报出错误
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();
//开始释放对象
        page->releaseUntil(stop);

        // memory: delete empty children
//        如果释放池页是空的直接清空,且上一个释放池页不是空,把上一个释放池页设置成hotpage当前页面
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            //如果这个释放池是空,上一个也是空,直接清除,设置当前释放池为空
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            
            // hysteresis: keep one empty child if page is more than half full
            //释放下一页的释放池
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

    static void init()
    {
        int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, 
                                             AutoreleasePoolPage::tls_dealloc);
        assert(r == 0);
    }

    void print() 
    {
        _objc_inform("[%p]  ................  PAGE %s %s %s", this, 
                     full() ? "(full)" : "", 
                     this == hotPage() ? "(hot)" : "", 
                     this == coldPage() ? "(cold)" : "");
        check(false);
        for (id *p = begin(); p < next; p++) {
            if (*p == POOL_BOUNDARY) {
                _objc_inform("[%p]  ################  POOL %p", p, p);
            } else {
                _objc_inform("[%p]  %#16lx  %s", 
                             p, (unsigned long)*p, object_getClassName(*p));
            }
        }
    }

    static void printAll()
    {        
        _objc_inform("##############");
        _objc_inform("AUTORELEASE POOLS for thread %p", pthread_self());

        AutoreleasePoolPage *page;
        ptrdiff_t objects = 0;
        for (page = coldPage(); page; page = page->child) {
            objects += page->next - page->begin();
        }
        _objc_inform("%llu releases pending.", (unsigned long long)objects);

        if (haveEmptyPoolPlaceholder()) {
            _objc_inform("[%p]  ................  PAGE (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
            _objc_inform("[%p]  ################  POOL (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
        }
        else {
            for (page = coldPage(); page; page = page->child) {
                page->print();
            }
        }

        _objc_inform("##############");
    }

    static void printHiwat()
    {
        // Check and propagate high water mark
        // Ignore high water marks under 256 to suppress noise.
        AutoreleasePoolPage *p = hotPage();
        uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());
        if (mark > p->hiwat  &&  mark > 256) {
            for( ; p; p = p->parent) {
                p->unprotect();
                p->hiwat = mark;
                p->protect();
            }
            
            _objc_inform("POOL HIGHWATER: new high water mark of %u "
                         "pending releases for thread %p:", 
                         mark, pthread_self());
            
            void *stack[128];
            int count = backtrace(stack, sizeof(stack)/sizeof(stack[0]));
            char **sym = backtrace_symbols(stack, count);
            for (int i = 0; i < count; i++) {
                _objc_inform("POOL HIGHWATER:     %s", sym[i]);
            }
            free(sym);
        }
    }

#undef POOL_BOUNDARY
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值