美文网首页
iOS内存管理(二)alloc、retain、release、d

iOS内存管理(二)alloc、retain、release、d

作者: 默默_David | 来源:发表于2020-06-28 18:21 被阅读0次

    iOS内存管理(一)、内存分区和引用计数
    iOS内存管理(二)alloc、retain、release、dealloc

    一. alloc源码分析

    + (id)alloc {
        return _objc_rootAlloc(self);
    }
    
    id _objc_rootAlloc(Class cls)
    {
        return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
    }
    
    static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
    {
    #if __OBJC2__
        //我们是OC2,所以会进入这里
        //关于slowpath和fastpath在下方会有解释,大致意思就是告诉编译器,
        //slowpath这里执行可能更小,而fastpath执行可能更大
        if (slowpath(checkNil && !cls)) return nil;
        if (fastpath(!cls->ISA()->hasCustomAWZ())) {
            //再调用_objc_rootAllocWithZone方法去实际的分配内存
            return _objc_rootAllocWithZone(cls, nil);
        }
    #endif
    
        // No shortcuts available.
        if (allocWithZone) {
            return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
        }
        return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
    }
    
    NEVER_INLINE id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
    {
        // allocWithZone under __OBJC2__ ignores the zone parameter
        return _class_createInstanceFromZone(cls, 0, nil,
                                             OBJECT_CONSTRUCT_CALL_BADALLOC);
    }
    
    //实际创建对象分配内存的方法
    static ALWAYS_INLINE id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                                  int construct_flags = OBJECT_CONSTRUCT_NONE,
                                  bool cxxConstruct = true,
                                  size_t *outAllocatedSize = nil)
    {
        ASSERT(cls->isRealized());
    
        // Read class's info bits all at once for performance
        bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
        bool hasCxxDtor = cls->hasCxxDtor();
        bool fast = cls->canAllocNonpointer();
        size_t size;
    
        //确定应该分配的内存
        size = cls->instanceSize(extraBytes);
        if (outAllocatedSize) *outAllocatedSize = size;
    
        id obj;
        if (zone) {
            //核心:这里开辟内存
            obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (slowpath(!obj)) {
            if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
                return _objc_callBadAllocHandler(cls);
            }
            return nil;
        }
    
        //初始化isa
        if (!zone && fast) {
            obj->initInstanceIsa(cls, hasCxxDtor);
        } else {
            // Use raw pointer isa on the assumption that they might be
            // doing something weird with the zone or RR.
            obj->initIsa(cls);
        }
    
        if (fastpath(!hasCxxCtor)) {
            return obj;
        }
    
        construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
        return object_cxxConstructFromClass(obj, cls, construct_flags);
    }
    
    //确定分配内存的大小
    size_t instanceSize(size_t extraBytes) const {
            if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
                return cache.fastInstanceSize(extraBytes);
            }
    
            size_t size = alignedInstanceSize() + extraBytes;
            // CF requires all objects be at least 16 bytes.
            /*
            这里可以看到一个OC对象初始化至少为16个字节
            iOS的指定对齐为8,所以OC对象的内存大小都是8的倍数
            */
            if (size < 16) size = 16;
            return size;
        }
    
    

    综上所述,我们可以得出如下结论:

    1. + (id)alloc方法调用_objc_rootAlloc方法
    2. _objc_rootAlloc调用callAlloc方法,callAlloc调用_objc_rootAllocWithZone_objc_rootAllocWithZone调用_class_createInstanceFromZone方法,这里实际完成对象的内存开辟
    3. OC对象的内存最小为16个字节(也就是说,一个NSObject对象,至少有16个字节)

    __builtin_expect

    我们对callAlloc(Class cls, bool checkNil, bool allocWithZone=false)函数中出现的slowpath作出一点说明,我们可以看到slowpath的定义

    #define fastpath(x) (__builtin_expect(bool(x), 1))
    #define slowpath(x) (__builtin_expect(bool(x), 0))
    

    __builtin这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)
    意思是:EXP==N的概率很大。

    一般的使用方法是将__builtin_expect指令封装为likelyunlikely宏。这两个宏的写法如下.

    #define likely(x) __builtin_expect(!!(x), 1) //x很可能为真       
    #define unlikely(x) __builtin_expect(!!(x), 0) //x很可能为假
    

    内核中的 likely()unlikely(),首先要明确:

    if(likely(value))  //等价于 if(value)
    if(unlikely(value))  //也等价于 if(value)
    

    __builtin_expect()GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。
    __builtin_expect((x),1)表示 x 的值为真的可能性更大;
    __builtin_expect((x),0)表示 x 的值为假的可能性更大。
    也就是说,使用likely(),执行 if 后面的语句的机会更大,使用 unlikely(),执行 else 后面的语句的机会更大。通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降。

    isa的初始化

    我们再看看isa的初始化

    inline void 
    objc_object::initIsa(Class cls)
    {
        initIsa(cls, false, false);
    }
    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
    { 
        ASSERT(!isTaggedPointer()); 
        
        if (!nonpointer) {
            isa = isa_t((uintptr_t)cls);
        } else {
            ASSERT(!DisableNonpointerIsa);
            ASSERT(!cls->instancesRequireRawIsa());
    
            isa_t newisa(0);
    
    #if SUPPORT_INDEXED_ISA
            ASSERT(cls->classArrayIndex() > 0);
            newisa.bits = ISA_INDEX_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.indexcls = (uintptr_t)cls->classArrayIndex();
    #else
            newisa.bits = ISA_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.shiftcls = (uintptr_t)cls >> 3;
    #endif
            isa = newisa;
        }
    }
    

    isa的初始化是在_class_createInstanceFromZone中,在完成对象内存的分配后,对isa进行初始化,而且从上面可以看出,isa初始化的时候,并没有存储1个引用计数,这就是为什么之前retainCount的源码中从数据中取出引用计数要加1的原因了

    init的源码简介

    + (id)init {
        return (id)self;
    }
    
    - (id)init {
        return _objc_rootInit(self);
    }
    
    id _objc_rootInit(id obj)
    {
        return obj;
    }
    

    从源码中可以看到,无论是哪种形式的init初始化,都是直接返回自身,并没有进行任何的操作

    new的源码

    我们再来看看new的源码

    + (id)new {
        return [callAlloc(self, false/*checkNil*/) init];
    }
    

    可以看出,new实际上也是先调用callAlloc完成内存分配,然后再调用init,所以它等同于alloc+init的调用

    二. retain源码分析

    //类方法的retain直接返回自身
    + (id)retain {
        return (id)self;
    }
    
    //实例方法的retain
    - (id)retain {
        return _objc_rootRetain(self);
    }
    
    NEVER_INLINE id
    _objc_rootRetain(id obj)
    {
        ASSERT(obj);
    
        return obj->rootRetain();
    }
    ALWAYS_INLINE id 
    objc_object::rootRetain()
    {
        return rootRetain(false, false);
    }
    
    #define RC_ONE   (1ULL<<56)
    #define RC_HALF  (1ULL<<7)
    ALWAYS_INLINE id 
    objc_object::rootRetain(bool tryRetain, bool handleOverflow)
    {
        if (isTaggedPointer()) return (id)this;
    
        bool sideTableLocked = false;
        bool transcribeToSideTable = false;
    
        isa_t oldisa;
        isa_t newisa;
    
        do {
            transcribeToSideTable = false;
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            if (slowpath(!newisa.nonpointer)) {
                ClearExclusive(&isa.bits);
                if (rawISA()->isMetaClass()) return (id)this;
                if (!tryRetain && sideTableLocked) sidetable_unlock();
                if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
                else return sidetable_retain();
            }
            // don't check newisa.fast_rr; we already called any RR overrides
            if (slowpath(tryRetain && newisa.deallocating)) {
                ClearExclusive(&isa.bits);
                if (!tryRetain && sideTableLocked) sidetable_unlock();
                return nil;
            }
            uintptr_t carry;
            //extra_rc中加1,carry判断是否溢出,RC_ONE是(1ULL<<56),正好是extra_rc的低位
            //从这里我们也可以看出,extra-rc最多能存储8位的引用计数值,也就是最多(2^8-1 = 255)位引用计数
            newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
    
            //如果上溢出
            if (slowpath(carry)) {
                // newisa.extra_rc++ overflowed
                if (!handleOverflow) {
                    ClearExclusive(&isa.bits);
                    return rootRetain_overflow(tryRetain);
                }
                // Leave half of the retain counts inline and 
                // prepare to copy the other half to the side table.
                if (!tryRetain && !sideTableLocked) sidetable_lock();
                sideTableLocked = true;
                transcribeToSideTable = true;
                //如果上溢出,那么把一半(2^7=128)留在extra_rc中,还有一半(2^7=128)存到sidetable中
                newisa.extra_rc = RC_HALF;
                //把有sidetable的标志位置为true
                newisa.has_sidetable_rc = true;
            }
            //判断存储是否完整
        } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            //将一半(2^7=128)存到sidetable中
            sidetable_addExtraRC_nolock(RC_HALF);
        }
    
        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
        return (id)this;
    }
    
    bool 
    objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
    {
        ASSERT(isa.nonpointer);
        SideTable& table = SideTables()[this];
    
        size_t& refcntStorage = table.refcnts[this];
        //先取出之前存储的引用计数值
        size_t oldRefcnt = refcntStorage;
        // isa-side bits should not be set here
        ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
        ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
    
        if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
    
        uintptr_t carry;
        //将旧值和新追加的值相加
        size_t newRefcnt = 
            addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
        if (carry) {
            refcntStorage =
                SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
            return true;
        }
        else {
            refcntStorage = newRefcnt;
            return false;
        }
    }
    
    

    如源码中所见,我们可以得出如下结论:

    1. 类方法的retain直接返回自身
    2. taggedpointer对象的retain直接返回自身
    3. 实例方法的retain调用步骤为:- (id)retain->_objc_rootRetain(self)->objc_object::rootRetain()->objc_object::rootRetain(false, false),关于引用计数的存储操作在objc_object::rootRetain中实现
    4. isa中最多存储(2^8-1 = 255)个引用计数值

    引用计数的存储步骤:

    1. 首先取出isa_t.bits,和RC_ONE(就是1)相加,如果没有上溢出,返回
    2. 如果发生了上溢出,则将isa中的引用计数值拿出一半(RC_HALF = 2^7 = 128)存储到sidetable中,一半留在isa中存储,并将isa位域中的has_sidetable_rc位置为true

    三. release源码分析

    //类方法的release什么也不做
    + (oneway void)release {
    }
    //实例方法中调用_objc_rootRelease
    - (oneway void)release {
        _objc_rootRelease(self);
    }
    
    NEVER_INLINE void _objc_rootRelease(id obj)
    {
        //判空
        ASSERT(obj);
    
        obj->rootRelease();
    }
    
    ALWAYS_INLINE bool objc_object::rootRelease()
    {
        return rootRelease(true, false);
    }
    
    ALWAYS_INLINE bool 
    objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
    {
        //TaggedPointer直接返回
        if (isTaggedPointer()) return false;
    
        bool sideTableLocked = false;
    
        isa_t oldisa;
        isa_t newisa;
    
     retry:
        do {
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            if (slowpath(!newisa.nonpointer)) {
                ClearExclusive(&isa.bits);
                if (rawISA()->isMetaClass()) return false;
                if (sideTableLocked) sidetable_unlock();
                return sidetable_release(performDealloc);
            }
            // don't check newisa.fast_rr; we already called any RR overrides
            uintptr_t carry;
            //将extra_rc减一
            newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
            //发现下溢出,跳转underflow
            if (slowpath(carry)) {
                // don't ClearExclusive()
                goto underflow;
            }
        } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                                 oldisa.bits, newisa.bits)));
    
        if (slowpath(sideTableLocked)) sidetable_unlock();
        return false;
    
     underflow:
        // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    
        // abandon newisa to undo the decrement
        newisa = oldisa;
    
        //如果sidetable存在
        if (slowpath(newisa.has_sidetable_rc)) {
            if (!handleUnderflow) {
                ClearExclusive(&isa.bits);
                return rootRelease_underflow(performDealloc);
            }
    
            // Transfer retain count from side table to inline storage.
    
            if (!sideTableLocked) {
                ClearExclusive(&isa.bits);
                sidetable_lock();
                sideTableLocked = true;
                // Need to start over to avoid a race against 
                // the nonpointer -> raw pointer transition.
                goto retry;
            }
    
            // Try to remove some retain counts from the side table.
            //从sidetable中借一半(2^7=128),用于之后放到extra_rc中
            size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
    
            // To avoid races, has_sidetable_rc must remain set 
            // even if the side table count is now zero.
    
            if (borrowed > 0) {
                // Side table retain count decreased.
                // Try to add them to the inline count.
                //将一半减去1,然后存到isa的extra_rc中
                newisa.extra_rc = borrowed - 1;  // redo the original decrement too
                bool stored = StoreReleaseExclusive(&isa.bits, 
                                                    oldisa.bits, newisa.bits);
                if (!stored) {
                    // Inline update failed. 
                    // Try it again right now. This prevents livelock on LL/SC 
                    // architectures where the side table access itself may have 
                    // dropped the reservation.
                    isa_t oldisa2 = LoadExclusive(&isa.bits);
                    isa_t newisa2 = oldisa2;
                    if (newisa2.nonpointer) {
                        uintptr_t overflow;
                        newisa2.bits = 
                            addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                        if (!overflow) {
                            stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                           newisa2.bits);
                        }
                    }
                }
    
                if (!stored) {
                    // Inline update failed.
                    // Put the retains back in the side table.
                    sidetable_addExtraRC_nolock(borrowed);
                    goto retry;
                }
    
                // Decrement successful after borrowing from side table.
                // This decrement cannot be the deallocating decrement - the side 
                // table lock and has_sidetable_rc bit ensure that if everyone 
                // else tried to -release while we worked, the last one would block.
                sidetable_unlock();
                return false;
            }
            else {
                // Side table is empty after all. Fall-through to the dealloc path.
            }
        }
    
        // Really deallocate.
    
        //判断是否正在析构
        if (slowpath(newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            //正在析构,返回一个错误
            return overrelease_error();
            // does not actually return
        }
        //extra_rc产生了下溢出,而没有sidetable,引用计数为0了,就该析构了
        //将析构的标记置为true
        newisa.deallocating = true;
        if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
    
        if (slowpath(sideTableLocked)) sidetable_unlock();
    
        __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
    
        //调用的时候performDealloc传过来就是true
        if (performDealloc) {
            //主动调起析构函数dealloc
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
        }
        return true;
    }
    
    

    release的源码我们可以得出如下结构

    1. 类方法的release什么也没做
    2. taggedpointer对象的release直接返回自身
    3. 实例方法的release调用步骤为:- (oneway void)release->_objc_rootRelease(self)->objc_object::rootRelease()->objc_object::rootRelease(true, false)

    objc_object::rootRelease函数中操作步骤:

    1. 首先在retry标签中对isa中的extra_rc减一,如果没有下溢出,返回
    2. 如果发生了下溢出(即extra_rc之前为0,减去1之后为-1),则跳转到underflow标签中
    3. underflow标签中先根据has_sidetable_rc判断是否有散列表,如果有散列表则在散列表中取出RC_HALF(2^7=128)减去1,再存入isa的extra_rc中,如果散列表有是空的,则跳到第4步
    4. 如果没有散列表,或者散列表中存储的引用计数为0,则将isa的deallocating析构中标志标记为true,接着发送消息调用析构方法dealloc

    问题:为什么每次都RC_HALF的取出呢?
    答:(1)128个,则不用每次调用哈希算法,频繁操作散列表;(2)1<<7正好是一个字节的长度,存取方便(刚好占用一条数据总线)

    四. dealloc源码分析

    //类方法什么也不做
    + (void)dealloc {
    }
    //实例方法调用_objc_rootDealloc
    - (void)dealloc {
        _objc_rootDealloc(self);
    }
    
    void _objc_rootDealloc(id obj)
    {
        ASSERT(obj);
    
        obj->rootDealloc();
    }
    
    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;  // fixme necessary?
    
        //如果不是优化后的isa,而且没有被弱引用,没有关联对象,没有C++的析构函数,并且没有sidetable,直接释放
        if (fastpath(isa.nonpointer  &&  
                     !isa.weakly_referenced  &&  
                     !isa.has_assoc  &&  
                     !isa.has_cxx_dtor  &&  
                     !isa.has_sidetable_rc))
        {
            assert(!sidetable_present());
            free(this);
        } 
        else {
            //否则调用object_dispose
            object_dispose((id)this);
        }
    }
    
    id object_dispose(id obj)
    {
        if (!obj) return nil;
        //先调用objc_destructInstance析构所有的成员
        objc_destructInstance(obj);    
        //再释放自身
        free(obj);
    
        return nil;
    }
    
    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor();
            bool assoc = obj->hasAssociatedObjects();
    
            // This order is important.
            //有C++的析构函数,调用
            if (cxx) object_cxxDestruct(obj);
            //移除关联对象
            if (assoc) _object_remove_assocations(obj);
            //移除weak
            obj->clearDeallocating();
        }
    
        return obj;
    }
    
    //移除对象的关联对象
    void _object_remove_assocations(id object)
    {
        //关联哈希表
        ObjectAssociationMap refs{};
    
        {
            AssociationsManager manager;
            AssociationsHashMap &associations(manager.get());
            AssociationsHashMap::iterator i = associations.find((objc_object *)object);
            if (i != associations.end()) {
                refs.swap(i->second);
                associations.erase(i);
            }
        }
    
        // release everything (outside of the lock).
        for (auto &i: refs) {
            i.second.releaseHeldValue();
        }
    }
    
    //移除weak
    inline void objc_object::clearDeallocating()
    {
        sidetable_clearDeallocating();
    }
    
    //移除sidetable中的weak
    void objc_object::sidetable_clearDeallocating()
    {
        SideTable& table = SideTables()[this];
    
        // clear any weak table items
        // clear extra retain count and deallocating bit
        // (fixme warn or abort if extra retain count == 0 ?)
        table.lock();
        RefcountMap::iterator it = table.refcnts.find(this);
        if (it != table.refcnts.end()) {
            if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            //找到到调用weak_clear_no_lock移除weak_table中的weak
                weak_clear_no_lock(&table.weak_table, (id)this);
            }
            table.refcnts.erase(it);
        }
        table.unlock();
    }
    
    void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
    
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == nil) {
            /// XXX shouldn't happen, but does with mismatched CF/objc
            //printf("XXX no entry for clear deallocating %p\n", referent);
            return;
        }
    
        // zero out references
        weak_referrer_t *referrers;
        size_t count;
        
        if (entry->out_of_line()) {
            referrers = entry->referrers;
            count = TABLE_SIZE(entry);
        } 
        else {
            referrers = entry->inline_referrers;
            count = WEAK_INLINE_COUNT;
        }
        
        for (size_t i = 0; i < count; ++i) {
            objc_object **referrer = referrers[i];
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable at %p holds %p instead of %p. "
                                 "This is probably incorrect use of "
                                 "objc_storeWeak() and objc_loadWeak(). "
                                 "Break on objc_weak_error to debug.\n", 
                                 referrer, (void*)*referrer, (void*)referent);
                    objc_weak_error();
                }
            }
        }
        
        weak_entry_remove(weak_table, entry);
    }
    
    

    如源码中可见,我们可以得出如下结论:

    1. 类方法的dealloc什么也不做
    2. 实例方法的dealloc调用步骤为:dealloc->_objc_rootDealloc->objc_object::rootDealloc()
    3. objc_object::rootDealloc()方法中,如果不是优化后的isa指针(nonpointer为0),而且没有被弱引用过,且没有关联对象,没有C++的析构函数,并且没有has_sidetable_rc为0,直接调用free释放对象,否则调用object_dispose
    4. object_dispose中,首先调用objc_destructInstance(obj)析构所有成员,然后再调用free释放对象

    objc_destructInstance(obj)的调用步骤:

    1. 如果有C++的析构函数,调用object_cxxDestruct(obj)
    2. 如果有关联对象,移除:_object_remove_assocations(obj)
    3. 调用obj->clearDeallocating()移除weak弱引用

    综上所述,其实dealloc就是先析构所有的成员(包括关联对象、弱引用、C++析构方法),然后直接调用free释放对象

    相关文章

      网友评论

          本文标题:iOS内存管理(二)alloc、retain、release、d

          本文链接:https://www.haomeiwen.com/subject/ofqbxktx.html