热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android强指针和弱指针

基类RefBase  Android中强指针和弱指针都要继承自RefBase类。  值得注意得是,Refbase类中有内部类weakref_type,有一个类型为weakref_im

基类RefBase

  Android中强指针和弱指针都要继承自RefBase类。
  值得注意得是,Refbase类中有内部类weakref_type,有一个类型为weakref_impl*的成员变量。weakref_impl类继承自Refbase类的内部类weakref_type。weakref_type内部也有一个RefBase*类型的成员mBase指向了对应的Refbase对象。weakref_type类使用mStrong记录强引用计数,使用mWeak记录弱引用计数,使用mFlags记录指针生命周期取决于什么。mStrong被初始化为INITIAL_STRONG_VALUE,在宏定义被定义为2的28次方,mWeak被初始化为0。强引用计数和弱引用计数决定了指针什么时候该被释放。其中,OBJECT_LIFETIME_STRONG(也就是默认初始化值)表示只受强引用计数影响,OBJECT_LIFETIME_WEAK表示同时受强引用计数和弱引用计数影响, OBJECT_LIFETIME_MASK是一个掩码值,后面会使用flag&OBJECT_LIFETIME_MASK的形式来获取flag的最低位数值。

/frameworks/rs/cpp/util/RefBase.h

enum {
OBJECT_LIFETIME_STROnG= 0x0000,
OBJECT_LIFETIME_WEAK = 0x0001,
OBJECT_LIFETIME_MASK = 0x0001
};

/frameworks/rs/cpp/util/RefBase.h

class RefBase
{
public:
void incStrong(const void* id) const;
void decStrong(const void* id) const;

void forceIncStrong(const void* id) const;

//! DEBUGGING ONLY: Get current strong ref count.
int32_t getStrongCount() const;

class weakref_type
{
public:
RefBase* refBase() const;

void incWeak(const void* id);
void decWeak(const void* id);

// acquires a strong reference if there is already one.
bool attemptIncStrong(const void* id);

// acquires a weak reference if there is already one.
// This is not always safe. see ProcessState.cpp and BpBinder.cpp
// for proper use.
bool attemptIncWeak(const void* id);

//! DEBUGGING ONLY: Get current weak ref count.
int32_t getWeakCount() const;

//! DEBUGGING ONLY: Print references held on object.
void printRefs() const;

//! DEBUGGING ONLY: Enable tracking for this object.
// enable -- enable/disable tracking
// retain -- when tracking is enable, if true, then we save a stack trace
// for each reference and dereference; when retain == false, we
// match up references and dereferences and keep only the
// outstanding ones.

void trackMe(bool enable, bool retain);
};

weakref_type* createWeak(const void* id) const;

weakref_type* getWeakRefs() const;

//! DEBUGGING ONLY: Print references held on object.
inline void printRefs() const { getWeakRefs()->printRefs(); }

//! DEBUGGING ONLY: Enable tracking of object.
inline void trackMe(bool enable, bool retain)
{
getWeakRefs()->trackMe(enable, retain);
}

typedef RefBase basetype;

protected:
RefBase();
virtual ~RefBase();

//! Flags for extendObjectLifetime()
enum {
OBJECT_LIFETIME_STROnG= 0x0000,
OBJECT_LIFETIME_WEAK = 0x0001,
OBJECT_LIFETIME_MASK = 0x0001
};

void extendObjectLifetime(int32_t mode);

//! Flags for onIncStrongAttempted()
enum {
FIRST_INC_STROnG= 0x0001
};

virtual void onFirstRef();
virtual void onLastStrongRef(const void* id);
virtual bool onIncStrongAttempted(uint32_t flags, const void* id);
virtual void onLastWeakRef(const void* id);

private:
friend class ReferenceMover;
static void moveReferences(void* d, void const* s, size_t n,
const ReferenceConverterBase& caster);

private:
friend class weakref_type;
class weakref_impl;

RefBase(const RefBase& o);
RefBase& operator=(const RefBase& o);

weakref_impl* const mRefs;
};

  RefBase的构造函数将this作为参数,new一个weakref_impl对象。

/frameworks/rs/cpp/util/RefBase.cpp

RefBase::RefBase()
: mRefs(new weakref_impl(this))
{
}

  weakref_impl构造函数将传入的RefBase保存在mBase中。

/frameworks/rs/cpp/util/RefBase.cpp

class RefBase::weakref_impl : public RefBase::weakref_type
{
public:
volatile int32_t mStrong;
volatile int32_t mWeak;
RefBase* const mBase;
volatile int32_t mFlags;
...
weakref_impl(RefBase* base)
: mStrong(INITIAL_STRONG_VALUE)
, mWeak(0)
, mBase(base)
, mFlags(0)
, mStrongRefs(NULL)
, mWeakRefs(NULL)
, mTrackEnabled(!!DEBUG_REFS_ENABLED_BY_DEFAULT)
, mRetain(false)
{
}
...

  RefBase的析构函数被定义成虚函数,在析构时防止只析构基类不析构派生类的情况发生。
/frameworks/rs/cpp/util/RefBase.cpp

RefBase::~RefBase()
{
if (mRefs->mStrOng== INITIAL_STRONG_VALUE) {
// we never acquired a strong (and/or weak) reference on this object.
delete mRefs;
} else {
// life-time of this object is extended to WEAK or FOREVER, in
// which case weakref_impl doesn't out-live the object and we
// can free it now.
if ((mRefs->mFlags & OBJECT_LIFETIME_MASK) != OBJECT_LIFETIME_STRONG) {
// It's possible that the weak count is not 0 if the object
// re-acquired a weak reference in its destructor
if (mRefs->mWeak == 0) {
delete mRefs;
}
}
}
// for debugging purposes, clear this.
const_cast(mRefs) = NULL;
}

强指针类sp

  sp类是一个模板类,模板参数必须继承自RefBase。sp类是对普通指针的封装,它内部有一个普通指针成员m_ptr,保存了这个普通指针,这个普通指针指向类型和模板参数相同。

/frameworks/rs/server/StrongPointer.h

template <typename T>
class sp
{
public:
inline sp() : m_ptr(0) { }

sp(T* other);
sp(const sp& other);
template<typename U> sp(U* other);
template<typename U> sp(const sp& other);

~sp();

// Assignment

sp& operator = (T* other);
sp& operator = (const sp& other);

template<typename U> sp& operator = (const sp& other);
template<typename U> sp& operator = (U* other);

//! Special optimization for use by ProcessState (and nobody else).
void force_set(T* other);

// Reset

void clear();

// Accessors

inline T& operator* () const { return *m_ptr; }
inline T* operator-> () const { return m_ptr; }
inline T* get() const { return m_ptr; }

// Operators

COMPARE(==)
COMPARE(!=)
COMPARE(>)
COMPARE(<)
COMPARE(<=)
COMPARE(>=)

private:
template<typename Y> friend class sp;
template<typename Y> friend class wp;
void set_pointer(T* ptr);
T* m_ptr;
};

  先看看sp实例是如何构造出来的。sp类将模板类型的参数保存在成员m_ptr中,然后调用模板参数类的父类RefBase的incStrong函数。

/frameworks/rs/server/StrongPointer.h

templateT>
sp<T>::sp(T* other)
: m_ptr(other)
{
if (other) other->incStrong(this);
}

  sp的this作为RefBase::incStrong的id参数传入,方便调试用。首先,创建一个和mRefs指向相同的指针,调用RefBase::weakref_type::incWeak方法。incWeak方法中,虽然调用的是weakref_type的incWeak方法,但是调用者refs本质上是weakref_impl*类型,所以weakref_type*类型的指针可以被安全强转成weakref_impl*类型的指针,从而对weakref_impl内部mWeak弱引用计数加1。android_atomic_inc返回的是加1前的值。回到RefBase::incStrong方法,接下来对强引用计数加1。前面提到,强引用计数初始化时是INITIAL_STRONG_VALUE,加1后需要减去INITIAL_STRONG_VALUE恢复正确值。最后,调用RefBase的onFirstRef,这是个空实现,由子类去实现,可以加入一些首次被强指针引用时的逻辑。

/system/core/libutils/RefBase.cpp

void RefBase::incStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
refs->incWeak(id);
//debug方法
refs->addStrongRef(id);
const int32_t c = android_atomic_inc(&refs->mStrong);
ALOG_ASSERT(c > 0, "incStrong() called on %p after last strong ref", refs);
#if PRINT_REFS
ALOGD("incStrong of %p from %p: cnt=%d\n", this, id, c);
#endif
if (c != INITIAL_STRONG_VALUE) {
return;
}

android_atomic_add(-INITIAL_STRONG_VALUE, &refs->mStrong);
refs->mBase->onFirstRef();
}

/system/core/libutils/RefBase.cpp

void RefBase::weakref_type::incWeak(const void* id)
{
weakref_impl* const impl = static_cast(this);
impl->addWeakRef(id);
const int32_t c __unused = android_atomic_inc(&impl->mWeak);
ALOG_ASSERT(c >= 0, "incWeak called on %p after last weak ref", this);
}

  可见,使用incStrong对强引用计数加1时,同时也会对弱引用计数加1。
  再看看sp的析构函数。可以看到是调用了RefBase的decStrong函数。

/frameworks/rs/server/StrongPointer.h

template<typename T>
sp::~sp()
{
if (m_ptr) m_ptr->decStrong(this);
}

  RefBase::decStrong方法中,使用android_atomic_dec对强引用计数减1,android_atomic_dec返回减1之前的值。如果此时强引用计数已变成0且指针生命周期仅取决于强引用计数,释放掉RefBase对象。最后,再对弱引用计数减1。可见,在使用decStrong对强引用计数减1的同时,也会对弱引用计数减1。

/frameworks/rs/cpp/util/RefBase.cpp

void RefBase::decStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = android_atomic_dec(&refs->mStrong);
#if PRINT_REFS
ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c);
#endif
ALOG_ASSERT(c >= 1, "decStrong() called on %p too many times", refs);
if (c == 1) {
refs->mBase->onLastStrongRef(id);
if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
refs->decWeak(id);
}

  看看RefBase的析构函数。如果对象的强引用计数为初始化值,说明从未被强指针引用过,释放掉mRefs成员。如果曾经被强指针引用过,且生命周期不是仅取决于强引用计数,那么当弱引用计数为0时,释放掉mRefs成员。因为弱引用计数总是随着强引用计数增加或减少的,弱引用计数不会少于强引用计数,当弱引用计数为0时,强引用计数必然为0。

RefBase::~RefBase()
{
if (mRefs->mStrOng== INITIAL_STRONG_VALUE) {
// we never acquired a strong (and/or weak) reference on this object.
delete mRefs;
} else {
// life-time of this object is extended to WEAK or FOREVER, in
// which case weakref_impl doesn't out-live the object and we
// can free it now.
if ((mRefs->mFlags & OBJECT_LIFETIME_MASK) != OBJECT_LIFETIME_STRONG) {
// It's possible that the weak count is not 0 if the object
// re-acquired a weak reference in its destructor
if (mRefs->mWeak == 0) {
delete mRefs;
}
}
}
// for debugging purposes, clear this.
const_cast(mRefs) = NULL;
}

弱指针类wp

  弱指针类wp和强指针类sp同样拥有被封装的指针成员m_ptr,还有一个weakref_type*类型的成员。

/frameworks/rs/cpp/util/RefBase.h

template <typename T>
class wp
{
public:
typedef typename RefBase::weakref_type weakref_type;

inline wp() : m_ptr(0) { }

wp(T* other);
wp(const wp& other);
wp(const sp& other);
template<typename U> wp(U* other);
template<typename U> wp(const sp& other);
template<typename U> wp(const wp& other);
...
private:
template<typename Y> friend class sp;
template<typename Y> friend class wp;

T* m_ptr;
weakref_type* m_refs;
};

  看看wp的构造函数。

/frameworks/rs/cpp/util/RefBase.h

templateT>
wp<T>::wp(T* other)
: m_ptr(other)
{
if (other) m_refs = other->createWeak(this);
}

/frameworks/rs/cpp/util/RefBase.h

template<typename T>
wp::wp(const sp& other)
: m_ptr(other.m_ptr)
{
if (m_ptr) {
m_refs = m_ptr->createWeak(this);
}
}

  createWeak调用RefBase::weakref_type::incWeak方法。

/frameworks/rs/cpp/util/RefBase.h

RefBase::weakref_type* RefBase::createWeak(const void* id) const
{
mRefs->incWeak(id);
return mRefs;
}

  incWeak函数对弱引用计数加1。

/frameworks/rs/cpp/util/RefBase.cpp

void RefBase::weakref_type::incWeak(const void* id)
{
weakref_impl* const impl = static_cast(this);
impl->addWeakRef(id);
const int32_t c __unused = android_atomic_inc(&impl->mWeak);
ALOG_ASSERT(c >= 0, "incWeak called on %p after last weak ref", this);
}

  可以发现,无论是在强指针中,还是在弱指针中,弱引用计数的数量都会大于或等于强引用计数的数量(排除未被强指针引用时强引用计数为INITIAL_STRONG_VALUE的情况)。
  看看wp的析构函数。

/frameworks/rs/cpp/util/RefBase.h

template<typename T>
wp::~wp()
{
if (m_ptr) m_refs->decWeak(this);
}

  decWeak函数中,首先会对弱引用计数减1。如果弱引用计数减1后不等于0,直接返回。若等于0,进入释放步骤。在释放步骤中,若对象的生命周期仅只受强引用计数影响且未被强指针引用过,释放掉对应的RefBase对象;若对象的生命周期只受强引用计数影响且曾被强指针引用过,此时因为弱引用计数为0,所以强引用计数也必定为0,只释放weakref_impl*指针,这里为什么不从RefBase开始释放呢?因为之前的decStrong函数已经释放过一次了。如果对象的生命周期同时受强引用计数和弱引用计数影响,释放掉对应的RefBase对象。

/frameworks/rs/cpp/util/RefBase.cpp

void RefBase::weakref_type::decWeak(const void* id)
{
weakref_impl* const impl = static_cast(this);
impl->removeWeakRef(id);
const int32_t c = android_atomic_dec(&impl->mWeak);
ALOG_ASSERT(c >= 1, "decWeak called on %p too many times", this);
if (c != 1) return;

if ((impl->mFlags&OBJECT_LIFETIME_WEAK) == OBJECT_LIFETIME_STRONG) {
// This is the regular lifetime case. The object is destroyed
// when the last strong reference goes away. Since weakref_impl
// outlive the object, it is not destroyed in the dtor, and
// we'll have to do it here.
if (impl->mStrOng== INITIAL_STRONG_VALUE) {
// Special case: we never had a strong reference, so we need to
// destroy the object now.
delete impl->mBase;
} else {
// ALOGV("Freeing refs %p of old RefBase %p\n", this, impl->mBase);
delete impl;
}
} else {
// less common case: lifetime is OBJECT_LIFETIME_{WEAK|FOREVER}
impl->mBase->onLastWeakRef(id);
if ((impl->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_WEAK) {
// this is the OBJECT_LIFETIME_WEAK case. The last weak-reference
// is gone, we can destroy the object.
delete impl->mBase;
}
}
}

弱指针提升promote

  弱指针是不能直接操作引用对象的,必须提升成强指针才能操作引用对象。因为wp类没有重载*和->运算符。wp指针可以通过promote函数来提升为强指针。

/frameworks/rs/cpp/util/RefBase.h

templateT>
sp<T> wp<T>::promote() const
{
sp<T> result;
if (m_ptr && m_refs->attemptIncStrong(&result)) {
result.set_pointer(m_ptr);
}
return result;
}

  attemptIncStrong函数中,首先使用incWeak函数对弱引用计数加1。curCount记录为对象的强引用计数。当对象有被强指针引用时(curCount>0且不为初始值),使用android_atomic_cmpxchg对curCount加1,这里使用while循环,若加1失败则恢复原值重新执行加1操作。若对象从没被强指针引用过或者强引用计数小于或等于0,这里依据生命周期进行分情况讨论:1.如果对象生命周期仅受强引用计数影响,当curCount小于或等于0时,说明对象已被释放,没有提升的可能了,直接使用decWeak对弱引用计数减1,因为函数在开始的时候对弱引用计数加了1。当对象没被强指针引用过时,对curCount加1。在加1的循环体内,若其他线程使对象的强引用计数变成0,则说明对象会被释放,调用decWeak对弱引用计数减1。2.如果对象生命周期为其他方式,调用onIncStrongAttempted尝试对强引用计数加1,onIncStrongAttempted是RefBase的一个虚函数,有子类重写,具体功能是是否允许对强引用计数加1。若不允许,对弱引用计数减1;若允许,则对强引用计数加1。
  上述有些情况会导致强引用计数大于或等于初始化值,之后便对这些值进行恢复,减去INITIAL_STRONG_VALUE即可。

/frameworks/rs/cpp/util/RefBase.cpp

bool RefBase::weakref_type::attemptIncStrong(const void* id)
{
incWeak(id);

weakref_impl* const impl = static_cast(this);
int32_t curCount = impl->mStrong;

ALOG_ASSERT(curCount >= 0,
"attemptIncStrong called on %p after underflow", this);

while (curCount > 0 && curCount != INITIAL_STRONG_VALUE) {
// we're in the easy/common case of promoting a weak-reference
// from an existing strong reference.
if (android_atomic_cmpxchg(curCount, curCount+1, &impl->mStrong) == 0) {
break;
}
// the strong count has changed on us, we need to re-assert our
// situation.
curCount = impl->mStrong;
}

if (curCount <= 0 || curCount == INITIAL_STRONG_VALUE) {
// we're now in the harder case of either:
// - there never was a strong reference on us
// - or, all strong references have been released
if ((impl->mFlags&OBJECT_LIFETIME_WEAK) == OBJECT_LIFETIME_STRONG) {
// this object has a "normal" life-time, i.e.: it gets destroyed
// when the last strong reference goes away
if (curCount <= 0) {
// the last strong-reference got released, the object cannot
// be revived.
decWeak(id);
return false;
}

// here, curCount == INITIAL_STRONG_VALUE, which means
// there never was a strong-reference, so we can try to
// promote this object; we need to do that atomically.
while (curCount > 0) {
if (android_atomic_cmpxchg(curCount, curCount + 1,
&impl->mStrong) == 0) {
break;
}
// the strong count has changed on us, we need to re-assert our
// situation (e.g.: another thread has inc/decStrong'ed us)
curCount = impl->mStrong;
}

if (curCount <= 0) {
// promote() failed, some other thread destroyed us in the
// meantime (i.e.: strong count reached zero).
decWeak(id);
return false;
}
} else {
// this object has an "extended" life-time, i.e.: it can be
// revived from a weak-reference only.
// Ask the object's implementation if it agrees to be revived
if (!impl->mBase->onIncStrongAttempted(FIRST_INC_STRONG, id)) {
// it didn't so give-up.
decWeak(id);
return false;
}
// grab a strong-reference, which is always safe due to the
// extended life-time.
curCount = android_atomic_inc(&impl->mStrong);
}

// If the strong reference count has already been incremented by
// someone else, the implementor of onIncStrongAttempted() is holding
// an unneeded reference. So call onLastStrongRef() here to remove it.
// (No, this is not pretty.) Note that we MUST NOT do this if we
// are in fact acquiring the first reference.
if (curCount > 0 && curCount impl->mBase->onLastStrongRef(id);
}
}

impl->addStrongRef(id);

#if PRINT_REFS
ALOGD("attemptIncStrong of %p from %p: cnt=%d\n", this, id, curCount);
#endif

// now we need to fix-up the count if it was INITIAL_STRONG_VALUE
// this must be done safely, i.e.: handle the case where several threads
// were here in attemptIncStrong().
curCount = impl->mStrong;
while (curCount >= INITIAL_STRONG_VALUE) {
ALOG_ASSERT(curCount > INITIAL_STRONG_VALUE,
"attemptIncStrong in %p underflowed to INITIAL_STRONG_VALUE",
this);
if (android_atomic_cmpxchg(curCount, curCount-INITIAL_STRONG_VALUE,
&impl->mStrong) == 0) {
break;
}
// the strong-count changed on us, we need to re-assert the situation,
// for e.g.: it's possible the fix-up happened in another thread.
curCount = impl->mStrong;
}

return true;
}

  返回到promote函数中,如果attemptIncStrong返回true提升指针成功,set_pointer函数将wp中的m_ptr成员保存在sp中,最后返回这个sp对象,指针提升完成。

/frameworks/rs/server/StrongPointer.h

templateT>
void sp<T>::set_pointer(T* ptr) {
m_ptr = ptr;
}

总结

  sp化后,强弱引用计数各增加1,sp析构后,强弱引用计数各减1。
  wp化后,弱引用计数增加1,wp析构后,弱引用计数减1。


推荐阅读
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 深入解析CAS机制:全面替代传统锁的底层原理与应用
    本文深入探讨了CAS(Compare-and-Swap)机制,分析了其作为传统锁的替代方案在并发控制中的优势与原理。CAS通过原子操作确保数据的一致性,避免了传统锁带来的性能瓶颈和死锁问题。文章详细解析了CAS的工作机制,并结合实际应用场景,展示了其在高并发环境下的高效性和可靠性。 ... [详细]
  • java解析json转Map前段时间在做json报文处理的时候,写了一个针对不同格式json转map的处理工具方法,总结记录如下:1、单节点单层级、单节点多层级json转mapim ... [详细]
  • 深入解析Java中的空指针异常及其预防策略
    空指针异常(NullPointerException,简称NPE)是Java编程中最常见的异常之一。尽管其成因显而易见,但开发人员往往容易忽视或未能及时采取措施。本文将详细介绍如何有效避免空指针异常,帮助开发者提升代码质量。 ... [详细]
  • 本文将深入探讨 iOS 中的 Grand Central Dispatch (GCD),并介绍如何利用 GCD 进行高效多线程编程。如果你对线程的基本概念还不熟悉,建议先阅读相关基础资料。 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • Android 自定义 RecycleView 左滑上下分层示例代码
    为了满足项目需求,需要在多个场景中实现左滑删除功能,并且后续可能在列表项中增加其他功能。虽然网络上有很多左滑删除的示例,但大多数封装不够完善。因此,我们尝试自己封装一个更加灵活和通用的解决方案。 ... [详细]
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 使用HTML和JavaScript实现视频截图功能
    本文介绍了如何利用HTML和JavaScript实现从远程MP4、本地摄像头及本地上传的MP4文件中截取视频帧,并展示了具体的实现步骤和示例代码。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 本地存储组件实现对IE低版本浏览器的兼容性支持 ... [详细]
  • DVWA学习笔记系列:深入理解CSRF攻击机制
    DVWA学习笔记系列:深入理解CSRF攻击机制 ... [详细]
  • POJ 2482 星空中的星星:利用线段树与扫描线算法解决
    在《POJ 2482 星空中的星星》问题中,通过运用线段树和扫描线算法,可以高效地解决星星在窗口内的计数问题。该方法不仅能够快速处理大规模数据,还能确保时间复杂度的最优性,适用于各种复杂的星空模拟场景。 ... [详细]
author-avatar
wodewodewoe
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有