为什么view的setOnClickListener引用Activity不会内存泄漏

学就完事

本文:https://www.jianshu.com/p/4b3aad1b8dba

前言

问:一个Activity实现了onClickListener,这时1个Button使用了setOnClickListener(this)
为什么不会出现内存泄露?

答:因为Activity销毁的时候会释放View引用对象,所以不会内存泄露啊 。

问:能不能在细点?

答:?_? ,厄,windowdetach所有的view,然后view逐个释放引用。

问:再细点。

答:。。。

什么是内存泄露

首先,什么是内存泄露?
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。
Java内存泄漏的根本原因是什么呢?
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

Android 中的内存泄露也是这个意思,比如当退出Activity的时候,如果由于Activity被其它生命周期更长的对象所持有引用,而无法释放其所持有的内存,就会导致内存泄露。

答题

  • Activity在销毁时肯定是会释放对应的资源的,比如将Activity出栈,移除Window上所有的View,释放View中的各种事件,最后Window啥的资源全置空。呼叫GC来一波回收。
  • 关于ViewActivity循环引用的问题,GC会通过可达性分析来解决循环引用的问题。
    GC会从GC roots对象节点向下遍历,将所有不可达的对象进行标记,标记后确认是否有必要执行其finalize()方法,如果没有,或者已经被执行,则回收。如果有,则进入一个专门的队列,等待执行。
    GC会对队列中的对象进行第二次小规模标记,如果对象没有在finalize()方法中重新与引用链关联,就会被回收。

兴趣

由于对于Activity在何时去释放View的了解不够细致,所以,后面篇幅写了大概从源码去追溯View释放的一个过程。有兴趣的可以一起看一下,如果我描述的不太对,欢迎大家评论区指正。
我会非常感激您的指导。

OnClickListener在View中的引用位置

首先,咱们得先知道最终setOnClickListener(this)this引用在View中如何被引用

##View#setOnClickListener

可以看到引用被赋值于getListenerInfo().mOnClickListener

    // android31  android.view.View#setOnClickListener
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

我们再去看看getListenerInfo()是谁

View#getListenerInfo

ListenerInfoView用来存储一系列listener的成员信息类

那我们基本就可以确定最终将我们传入的OnClickListener引用在mListenerInfo中。

    // android31  android.view.View#getListenerInfo
    ListenerInfo mListenerInfo;
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

    static class ListenerInfo {
            ...
            public OnClickListener mOnClickListener;
            ...
    }

探索该引用何时被释放

View中查找mListenerInfo的使用,以及何时被置空释放。

在文件中检索mListenerInfo = null 或者 getListenerInfo = null 或者其它类似操作。

显然,我没有找到类似的方法和操作,哈哈哈,好尴尬。

既然它没有,那我们就接着找找View是什么时候被释放引用的,如果View的引用被释放,那他引用的这个对象也属于无效对象了,仍可以被释放。

Activity#onDestroy

既然是销毁的过程,咱就是Activity.onDestroy方法开始。

  • 发现通过mManagedDialogs关闭所有弹框
  • 发现通过mManagedCursors 关闭所有cursor
  • 发现通过mSearchManager 停止搜索
  • 发现通过mActionBar 销毁ActionBar
  • 发现通过dispatchActivityDestroyed分发ActivityLifecycleCallbacks
  • 发现通过notifyContentCaptureManagerIfNeeded 哎呀,不知道做什么的。回头补补
  • 发现通过mUiTranslationController分发UiTranslationStateCallback
    // android31 android.app.Activity#onDestroy
    @CallSuper
    protected void onDestroy() {
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this);
        mCalled = true;

        // dismiss any dialogs we are managing.
        if (mManagedDialogs != null) {
            final int numDialogs = mManagedDialogs.size();
            for (int i = 0; i < numDialogs; i++) {
                final ManagedDialog md = mManagedDialogs.valueAt(i);
                if (md.mDialog.isShowing()) {
                    md.mDialog.dismiss();
                }
            }
            mManagedDialogs = null;
        }

        // close any cursors we are managing.
        synchronized (mManagedCursors) {
            int numCursors = mManagedCursors.size();
            for (int i = 0; i < numCursors; i++) {
                ManagedCursor c = mManagedCursors.get(i);
                if (c != null) {
                    c.mCursor.close();
                }
            }
            mManagedCursors.clear();
        }

        // Close any open search dialog
        if (mSearchManager != null) {
            mSearchManager.stopSearch();
        }

        // SDK 24 Add , But it Add in 19 ,and remove in 20
        if (mActionBar != null) {
            mActionBar.onDestroy();
        }

        dispatchActivityDestroyed();

        // SDK 29 Add 
        notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_STOP);

        // SDK 31 Add
        if (mUiTranslationController != null) {
            mUiTranslationController.onActivityDestroyed();
        }
    }

唉,没看到windowview相关操作呢。
那咱往上找一下。

Activity#performDestroy

哎~ 我们看到一句合适的代码mWindow.destroy
其它的暂时先不看,我们直接去看mWindow.destroy干了什么

    // android31 android.app.Activity#performDestroy
    final void performDestroy() {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performDestroy:"
                    + mComponent.getClassName());
        }
        dispatchActivityPreDestroyed();
        mDestroyed = true;
        mWindow.destroy();
        mFragments.dispatchDestroy();
        onDestroy();
        EventLogTags.writeWmOnDestroyCalled(mIdent, getComponentName().getClassName(),
                "performDestroy");
        mFragments.doLoaderDestroy();
        if (mVoiceInteractor != null) {
            mVoiceInteractor.detachActivity();
        }
        dispatchActivityPostDestroyed();
        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
    }

确认下mWindow是谁,找到初始化的地方

    // android31 android.app.Activity#attach
    @UnsupportedAppUsage
    private Window mWindow;

    final void attach(Context context, ...) {
        ...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }

PhoneWindow并没有重写destroy

所以最终还是走了继承自Windowdestroy方法

    // android31 android.view.Window#destroy
    /** @hide */
    public final void destroy() {
        mDestroyed = true;
    }

好吧,依然没看到什么有用的信息。
咱么接着向上扒!
看看谁调用的performDestroy

Instrumentation#callActivityOnDestroy

比较眼熟的Instrumentation类哈
没有合适的信息,继续向上

    // android-31 android.app.Instrumentation#callActivityOnDestroy
    public void callActivityOnDestroy(Activity activity) {
      ...
      activity.performDestroy();
    }

ActivityThread#performDestroyActivity

我们暂时剃掉了部分影响不大的代码,直接从mInstrumentation.callActivityOnDestroy之后的流程继续看

  • r.window.closeAllPanels(),移除系统菜单(ActionBar菜单)View
  • mActivities.remove(r.token); ,移除Activity引用
  • 其它释放操作
    // android-31 android.app.ActivityThread#performDestroyActivity
    /** Core implementation of activity destroy call. */
    void performDestroyActivity(ActivityClientRecord r, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {

        ...
        try {
            r.activity.mCalled = false;
            mInstrumentation.callActivityOnDestroy(r.activity);
            if (!r.activity.mCalled) {
                throw new SuperNotCalledException("Activity " + safeToComponentShortString(r.intent)
                        + " did not call through to super.onDestroy()");
            }
            if (r.window != null) {
                r.window.closeAllPanels();
            }
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException("Unable to destroy activity "
                        + safeToComponentShortString(r.intent) + ": " + e.toString(), e);
            }
        }
        r.setState(ON_DESTROY);
        mLastReportedWindowingMode.remove(r.activity.getActivityToken());
        schedulePurgeIdler();
        synchronized (this) {
            if (mSplashScreenGlobal != null) {
                mSplashScreenGlobal.tokenDestroyed(r.token);
            }
        }
        // updatePendingActivityConfiguration() reads from mActivities to update
        // ActivityClientRecord which runs in a different thread. Protect modifications to
        // mActivities to avoid race.
        synchronized (mResourcesManager) {
            mActivities.remove(r.token);
        }
        StrictMode.decrementExpectedActivityCount(activityClass);
    }

感觉还不够啊,继续向上扒

ActivityThread#handleDestroyActivity


    @Override
    public void handleDestroyActivity(ActivityClientRecord r, boolean finishing, int configChanges,
            boolean getNonConfigInstance, String reason) {
        performDestroyActivity(r, finishing, configChanges, getNonConfigInstance, reason);
        cleanUpPendingRemoveWindows(r, finishing);
        WindowManager wm = r.activity.getWindowManager();
        View v = r.activity.mDecor;
        if (v != null) {
            if (r.activity.mVisibleFromServer) {
                mNumVisibleActivities--;
            }
            IBinder wtoken = v.getWindowToken();
            if (r.activity.mWindowAdded) {
                if (r.mPreserveWindow) {
                    // Hold off on removing this until the new activity's window is being added.
                    r.mPendingRemoveWindow = r.window;
                    r.mPendingRemoveWindowManager = wm;
                    // We can only keep the part of the view hierarchy that we control,
                    // everything else must be removed, because it might not be able to
                    // behave properly when activity is relaunching.
                    r.window.clearContentView();
                } else {
                    wm.removeViewImmediate(v);
                }
            }
            if (wtoken != null && r.mPendingRemoveWindow == null) {
                WindowManagerGlobal.getInstance().closeAll(wtoken,
                        r.activity.getClass().getName(), "Activity");
            } else if (r.mPendingRemoveWindow != null) {
                // We're preserving only one window, others should be closed so app views
                // will be detached before the final tear down. It should be done now because
                // some components (e.g. WebView) rely on detach callbacks to perform receiver
                // unregister and other cleanup.
                WindowManagerGlobal.getInstance().closeAllExceptView(r.token, v,
                        r.activity.getClass().getName(), "Activity");
            }
            r.activity.mDecor = null;
        }
        if (r.mPendingRemoveWindow == null) {
            // If we are delaying the removal of the activity window, then
            // we can't clean up all windows here.  Note that we can't do
            // so later either, which means any windows that aren't closed
            // by the app will leak.  Well we try to warning them a lot
            // about leaking windows, because that is a bug, so if they are
            // using this recreate facility then they get to live with leaks.
            WindowManagerGlobal.getInstance().closeAll(r.token,
                    r.activity.getClass().getName(), "Activity");
        }

        // Mocked out contexts won't be participating in the normal
        // process lifecycle, but if we're running with a proper
        // ApplicationContext we need to have it tear down things
        // cleanly.
        Context c = r.activity.getBaseContext();
        if (c instanceof ContextImpl) {
            ((ContextImpl) c).scheduleFinalCleanup(r.activity.getClass().getName(), "Activity");
        }
        if (finishing) {
            ActivityClient.getInstance().activityDestroyed(r.token);
        }
        mSomeActivitiesChanged = true;
    }


到此处,window开始移除所有的子view
我们不太确定是哪个方法最终生效了。

但是我们可以借助Debug Log,通过监控Viewdetached事件来确定流程。

如下图

view detached

哎呀呀呀,你看,后面的流程很明白了。

  • wm.removeViewImmediate(v) 调用 WindowManagerImpl#removeViewImmediate 方法
    // android-30 android.view.WindowManagerImpl#removeViewImmediate
    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }
  • 然后进入mGlobal.removeView(view, true);方法就是WindowManagerGlobal#removeView
  • 调用removeViewLocked(index, immediate);
    // android-30 android.view.WindowManagerGlobal#removeView
    @UnsupportedAppUsage
    public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }

    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (root != null) {
            root.getImeFocusController().onWindowDismissed();
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

  • removeViewLocked方法中进入ViewRootImpl#die方法
  • ViewRootImpl#die方法中执行ViewRootImpl#doDie开始执行die操作
  • ViewRootImpl#doDie中调用ViewRootImpl#dispatchDetachedFromWindow
  • ViewRootImpl#dispatchDetachedFromWindow中终于mView.dispatchDetachedFromWindow();
  • 我们可以去看viewdispatchDetachedFromWindow
    /**
     * @param immediate True, do now if not in traversal. False, put on queue and do later.
     * @return True, request has been queued. False, request has been completed.
     */
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

    void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();
            }

            if (mAdded && !mFirst) {
                destroyHardwareRenderer();

                if (mView != null) {
                    int viewVisibility = mView.getVisibility();
                    boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                    if (mWindowAttributesChanged || viewVisibilityChanged) {
                        // If layout params have been changed, first give them
                        // to the window manager to make sure it has the correct
                        // animation info.
                        try {
                            if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                    & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                                mWindowSession.finishDrawing(
                                        mWindow, null /* postDrawTransaction */);
                            }
                        } catch (RemoteException e) {
                        }
                    }

                    destroySurface();
                }
            }

            mAdded = false;
        }
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

    void dispatchDetachedFromWindow() {
        // Make sure we free-up insets resources if view never received onWindowFocusLost()
        // because of a die-signal
        mInsetsController.onWindowFocusLost();
        mFirstInputStage.onDetachedFromWindow();
        if (mView != null && mView.mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
            mView.dispatchDetachedFromWindow();
        }

        mAccessibilityInteractionConnectionManager.ensureNoConnection();
        mAccessibilityManager.removeAccessibilityStateChangeListener(
                mAccessibilityInteractionConnectionManager);
        mAccessibilityManager.removeHighTextContrastStateChangeListener(
                mHighContrastTextManager);
        removeSendWindowContentChangedCallback();

        destroyHardwareRenderer();

        setAccessibilityFocus(null, null);

        mInsetsController.cancelExistingAnimations();

        mView.assignParent(null);
        mView = null;
        mAttachInfo.mRootView = null;

        destroySurface();

        if (mInputQueueCallback != null && mInputQueue != null) {
            mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
            mInputQueue.dispose();
            mInputQueueCallback = null;
            mInputQueue = null;
        }
        try {
            mWindowSession.remove(mWindow);
        } catch (RemoteException e) {
        }
        // Dispose receiver would dispose client InputChannel, too. That could send out a socket
        // broken event, so we need to unregister the server InputChannel when removing window to
        // prevent server side receive the event and prompt error.
        if (mInputEventReceiver != null) {
            mInputEventReceiver.dispose();
            mInputEventReceiver = null;
        }

        mDisplayManager.unregisterDisplayListener(mDisplayListener);

        unscheduleTraversals();
    }

View.dispatchDetachedFromWindow

这条线,稍微有些不同。
开始做detached相关工作,并在onDetachedFromWindowInternal方法中释放了部分事件传递的资源


    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchDetachedFromWindow() {
        AttachInfo info = mAttachInfo;
        if (info != null) {
            int vis = info.mWindowVisibility;
            if (vis != GONE) {
                onWindowVisibilityChanged(GONE);
                if (isShown()) {
                    // Invoking onVisibilityAggregated directly here since the subtree
                    // will also receive detached from window
                    onVisibilityAggregated(false);
                }
            }
        }

        onDetachedFromWindow();
        onDetachedFromWindowInternal();

        if (info != null) {
            info.mViewRootImpl.getImeFocusController().onViewDetachedFromWindow(this);
        }

        ListenerInfo li = mListenerInfo;
        final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
                li != null ? li.mOnAttachStateChangeListeners : null;
        if (listeners != null && listeners.size() > 0) {
            // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
            // perform the dispatching. The iterator is a safe guard against listeners that
            // could mutate the list by calling the various add/remove methods. This prevents
            // the array from being modified while we iterate it.
            for (OnAttachStateChangeListener listener : listeners) {
                listener.onViewDetachedFromWindow(this);
            }
        }

        if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
            mAttachInfo.mScrollContainers.remove(this);
            mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;
        }

        mAttachInfo = null;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchDetachedFromWindow();
        }

        notifyEnterOrExitForAutoFillIfNeeded(false);
        notifyAppearedOrDisappearedForContentCaptureIfNeeded(false);
    }

View.onDetachedFromWindowInternal

可以看到这个方法中,我们remove了很多操作
比如removeUnsetPressCallbackremoveLongPressCallbackremovePerformClickCallback
clearAccessibilityThrottles

    @CallSuper
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected void onDetachedFromWindowInternal() {
        mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
        mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
        mPrivateFlags3 &= ~PFLAG3_TEMPORARY_DETACH;

        removeUnsetPressCallback();
        removeLongPressCallback();
        removePerformClickCallback();
        clearAccessibilityThrottles();
        stopNestedScroll();

        // Anything that started animating right before detach should already
        // be in its final state when re-attached.
        jumpDrawablesToCurrentState();

        destroyDrawingCache();

        cleanupDraw();
        mCurrentAnimation = null;

        if ((mViewFlags & TOOLTIP) == TOOLTIP) {
            hideTooltip();
        }

        AccessibilityNodeIdManager.getInstance().unregisterViewWithId(getAccessibilityViewId());
    }

为什么要remove这些Callback,他们做了什么,咱们接着查

View.removePerformClickCallback

这里可以看到,判断了mPerformClick != null 就会去remove

   /**
     * Remove the pending click action
     */
    @UnsupportedAppUsage
    private void removePerformClickCallback() {
        if (mPerformClick != null) {
            removeCallbacks(mPerformClick);
        }
    }

removeCallbacks函数做了什么?
移除了一些特殊的message操作,我们看到了attachInfo.mHandler,也就是说,如果没有取消的话,确实可能存在内存泄露的风险,具体要看这个mHandler是怎么实现得。

    /**
     * <p>Removes the specified Runnable from the message queue.</p>
     *
     * @param action The Runnable to remove from the message handling queue
     *
     * @return true if this view could ask the Handler to remove the Runnable,
     *         false otherwise. When the returned value is true, the Runnable
     *         may or may not have been actually removed from the message queue
     *         (for instance, if the Runnable was not in the queue already.)
     *
     * @see #post
     * @see #postDelayed
     * @see #postOnAnimation
     * @see #postOnAnimationDelayed
     */
    public boolean removeCallbacks(Runnable action) {
        if (action != null) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                attachInfo.mHandler.removeCallbacks(action);
                attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                        Choreographer.CALLBACK_ANIMATION, action, null);
            }
            getRunQueue().removeCallbacks(action);
        }
        return true;
    }

哈。跑深了哈,不过确认了子ViewdetachedFromWindow之后确实释放了很多东西,移除了所有可能延迟执行的Callback,关闭动画等,取消UI操作啥的。

回归上一个方法ActivityThread#handleDestroyActivity方法中做了如上的操作,那么我们继续向上看看,还做了什么操作

ActivityThread#handleRelaunchActivityInner

这个方法很多明显的置空操作了。

  • r.activity = null;
  • r.window = null;
  • r.nextIdle = null;
    // android-30 android.app.ActivityThread#handleRelaunchActivityInner
    private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
            List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
            PendingTransactionActions pendingActions, boolean startsNotResumed,
            Configuration overrideConfig, String reason) {
        // Preserve last used intent, it may be set from Activity#setIntent().
        final Intent customIntent = r.activity.mIntent;
        // Need to ensure state is saved.
        if (!r.paused) {
            performPauseActivity(r, false, reason, null /* pendingActions */);
        }
        if (!r.stopped) {
            callActivityOnStop(r, true /* saveState */, reason);
        }

        handleDestroyActivity(r.token, false, configChanges, true, reason);

        r.activity = null;
        r.window = null;
        r.hideForNow = false;
        r.nextIdle = null;
        // Merge any pending results and pending intents; don't just replace them
        if (pendingResults != null) {
            if (r.pendingResults == null) {
                r.pendingResults = pendingResults;
            } else {
                r.pendingResults.addAll(pendingResults);
            }
        }
        if (pendingIntents != null) {
            if (r.pendingIntents == null) {
                r.pendingIntents = pendingIntents;
            } else {
                r.pendingIntents.addAll(pendingIntents);
            }
        }
        r.startsNotResumed = startsNotResumed;
        r.overrideConfig = overrideConfig;

        handleLaunchActivity(r, pendingActions, customIntent);
    }

到这里,基本window,view都被释放空了。

参考文献

暂无

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值