Window内部机制与创建过程

引言

Window表示一个窗口的概念,它是一个抽象类,它的具体实现是PhoneWindow。可以通过WindowManager来创建一个Window。所以WindowManager是外界访问Windows的入口,Window 的具体实现位于WindowManagerService中, WindowManager和WindowManagerService的交互是一个IPC(Inter-Proscess Communication的缩写,含义为进程间的通讯或者跨进程通讯,是指两个进程之间进行数据交换的过程)过程。Android 中所有视图都是通过Window来呈现的,不管是Activity、Dialog 还是 Toast, 它们的视图都是附加在Window上的,所以Window是View的直接管理者。

为了分析Window 工作机制,先来了解下如何使用WindowManager添加一个Window,如下:

mBtuuton = new Button(getApplicationContext());  
WindowManager wmManager=(WindowManager) getSystemService(Context.WINDOW_SERVICE);  
WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams();  

wmParams.type=2002;//这里是关键,你也可以试试2003
wmParams.format=1;
wmParams.flags=40; //关键 代码实际是wmParams.flags |= FLAG_NOT_FOCUSABLE; 40的由来是wmParams的默认属性(32)+FLAG_NOT_FOCUSABLE(8) 
wmParams.width=40; 
wmParams.height=40; 
wmManager.addView(bb, wmParams);  //创建View  

对上述代码中比较重要的两个参数, flags和type 。首先分析flages如下归纳

  • FLAG_NOT_FOCUSABLE 表示 Window 不需要获取焦点,也不需要接受各种输入事件,此标记会同时启动FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层具有焦点的Window
  • FLAG_NOT_TOUCH_MODAL 系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理,这个标记很重要,一般来说都需要开启,否则其他Window将无法接收到单击事件
  • FLAG_SHOW_WHEN_LOCKED 开启此模式可以让Window 显示在锁屏的界面上

Type参数表示Window的类型,分为应用Window、 子Window、 系统Window。 应用类Window对应着一个Activity, 子Window不能单独存在,需要附属在特定的父Window之中,如常见的一些Dialog, 系统Window 是需要声明权限再能创建的Window,比如Toast和系统状态栏。
Window 是分层的,每个Window 都有对应的z-ordered, 层级大的会覆盖在层级小的Window上面,可以根据需要设置

WindowManager所提供的功能很简单,常见的只有三种,即添加View、更新View和删除View,这三个方法定义在ViewManager中,而WindowManager 继承了ViewManager

public  interface ViewManager 
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

正文

Window 的内部机制

Window 是一个抽象概念,每一个Window 对应着一个View 和一个 ViewRootImpl, Window 和View 通过ViewRootImpl 来建立联系的,
因此Window是以View的形式存在的。从上面WindowManager的定义方法可以看出,都是针对View 进行操作的, 所以View才是Window的实体。
为了分析Window的内部机制,现在从Window的添加、删除和更新说起。

1. Window 的添加过程

Window的添加过程是通过WindowManager的addView来实现的,WindowManager是一个接口,真正实现是WindowManagerImpl类,如下:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
    mGlobal.removeView(view, false);
}

可以看到WindowManagerImpl并没有直接实现Window的三大操作,是由WindowManagerGlobal来处理的

在WindowManagerGlobal中由几个列表比较重要

private final ArrayList<View> mViews = new ArrayList<View>();//存储所有Window对应的View
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();//存储所有Window对应的ViewRootImpl
private final ArrayList<WindowManager.LayoutParams> mParams =
        new ArrayList<WindowManager.LayoutParams>();//存储所有Window对应的布局参数
private final ArraySet<View> mDyingViews = new ArraySet<View>();//存储那些正在被删除的View对象

然后通过WindowManagerGlobal的addViews方法将Window的一系列对象添加到列表中

接着通过ViewRootImpl的setView方法来完成新界面的更新及Window的添加过程

接着通过WindowSession 最终完成Window的添加过程。 mWindowSession的类型是IWindowSession,它是一个Binder对象,真正实现的是Session,也就是说Window的添加过程是一次IPC的调用

try {
          mOrigWindowType = mWindowAttributes.type;
          mAttachInfo.mRecomputeGlobalAttributes = true;
          collectViewAttributes();
          res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
          mAttachInfo.mOutsets, mInputChannel); //mWindowSession是一个Binder对象
 } catch (RemoteException e) {
          mAdded = false;
          mView = null;
          mAttachInfo.mRootView = null;
          mInputChannel = null;
          mFallbackEventHandler.setView(null);
          unscheduleTraversals();
          setAccessibilityFocus(null, null);
           throw new RuntimeException("Adding window failed", e);
  } finally {
       if (restore) {
           attrs.restore();
         }
  }

Session内部addToDisplay方法会通过WindowManagerService来实现Window的添加。

2. Window 的删除过程

Window的删除和添加过程一样,都是先通过WindowManagerImpl后,再进一步通过WindowManagerGlobal来实现。 如下是WindowManagerGlobal中removeView的实现:

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 int findViewLocked(View view, boolean required) {
    final int index = mViews.indexOf(view);
    if (required && index < 0) {
        throw new IllegalArgumentException("View=" + view + " not attached to window manager");
    }
    return index;
}



private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
    if (view != null) {
        InputMethodManager imm = InputMethodManager.getInstance();
        if (imm != null) {
            imm.windowDismissed(mViews.get(index).getWindowToken());
        }
    }
    boolean deferred = root.die(immediate);//
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}

所以从上述可以看到,整个过程首先通过findViewLocked来查找待删除的View的索引,这个查找过程就是建立数组遍历,然后再调用removeViewLocked来做进一步的删除。在WindowManager中提供了两种删除接口removeView和removeViewImmediate,分别代表异步删除和同步删除,其中removeViewImmediate使用起来需要特别注意,具体操作是由ViewRootImpl的die方法来完成的,die方法发送了一个删除的消息后就立即返回了,这时View并没有完成删除操作,所以最后会将其添加到mDyingViews中,mDyingViews 表示待删除的View列表。

/*** @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;
}

异步删除这个消息发出去后,由ViewRootImpl的Handler 会处理并调用doDie方法

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);
                        }
                    } catch (RemoteException e) {
                    }
                }
                mSurface.release();
            }
        }
        mAdded = false;
    }
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

再来看下dispatchDetachedFromWindow的源码,这个方法主要销毁Window中的各个成员变量,临时变量等

void dispatchDetachedFromWindow() {
    if (mView != null && mView.mAttachInfo != null) {
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
        mView.dispatchDetachedFromWindow();//mView去实现
    }

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

    destroyHardwareRenderer();

    setAccessibilityFocus(null, null);

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

    mSurface.release();

    if (mInputQueueCallback != null && mInputQueue != null) {
        mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
        mInputQueue.dispose();
        mInputQueueCallback = null;
        mInputQueue = null;
    }
    if (mInputEventReceiver != null) {
        mInputEventReceiver.dispose();
        mInputEventReceiver = null;
    }
    try {
        mWindowSession.remove(mWindow);//删除
    } catch (RemoteException e) {
    }

    // Dispose the input channel after removing the window so the Window Manager
    // doesn't interpret the input channel being closed as an abnormal termination.
    if (mInputChannel != null) {
        mInputChannel.dispose();
        mInputChannel = null;
    }

    mDisplayManager.unregisterDisplayListener(mDisplayListener);

    unscheduleTraversals();
}

3. Window 的更新过程

分析它的更新过程,从 WindowManagerGlobal 的updateViewLayout 方法开始,如下

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

    view.setLayoutParams(wparams);//更新View的LayoutParams并替换老的LayoutParams

    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        root.setLayoutParams(wparams, false);
    }
}

Window 的创建过程

View是Android 中视图的呈现方式,但是View不能单独存在,必须依附在Window这个抽象概念上面,因此有视图的地方就有Window。

1. Activity 的 Window 创建过程

Activity的启动过程比较复杂,最终由ActivityThread 中的 performLaunchActivity 来完成整个启动过程

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);//类加载器创建Activity实例对象
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }

    ...
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
    ...
        if (activity != null) {
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                    + r.activityInfo.name + " with config " + config);
            activity.attach(appContext, this, getInstrumentation(), r.token,
              r.ident, app, r.intent, r.activityInfo, title, r.parent,
              r.embeddedID, r.lastNonConfigurationInstances, config,
              r.referrer, r.voiceInteractor);//attach为其关联运行过程中所依赖的上下文环境变量 

    ...

    return activity;
}

那么Activity中的attach 方法具体是做什么的呢? 如下:

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }
    if (info.uiOptions != 0) {
        mWindow.setUiOptions(info.uiOptions);
    }
    mUiThread = Thread.currentThread();

    mMainThread = aThread;
    mInstrumentation = instr;
    mToken = token;
    mIdent = ident;
    mApplication = application;
    mIntent = intent;
    mReferrer = referrer;
    mComponent = intent.getComponent();
    mActivityInfo = info;
    mTitle = title;
    mParent = parent;
    mEmbeddedID = id;
    mLastNonConfigurationInstances = lastNonConfigurationInstances;
    if (voiceInteractor != null) {
        if (lastNonConfigurationInstances != null) {
            mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
        } else {
            mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                    Looper.myLooper());
        }
    }

    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    mWindowManager = mWindow.getWindowManager();
    mCurrentConfig = config;
}

可以看到上面方法主要初始化了一些成员变量,主要是mWindow 对象,并且mWindow的成员实例是PhoneWindow实例,这样也从侧面说明了一个Activity对应着一个Window对象, Activity 的视图由setContentView方法提供,具体实现如下

public void setContentView(int layoutResID){
    getWindow().setContentView(layoutResID)l;
    initWindowDecorActionBar();
}

所以Activity将具体实现交给了Window处理,而Window的具体实现是PhoneWindow,所以只需要看PhoneWindow的相关逻辑即可。 从PhoneWindow的setContentView方法大致可以遵循如下几个步骤

  • 如果没有DecorView,那么就创建它

DecorView是一个FrameLayout,是Activity的顶级View,一般来说它的内部包括标题栏和内部栏,但会随主题的变换而发生改变,但是内容栏是一定要存在的,并用android.R.id.content。 DecorView的创建过程是由installDecor方法来完成的

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();//如果没有DecorView,那么就创建它
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);//将View添加到DecorView的mContentParent中
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();//回调Activity的onContentChanged方法通知Activity视图已经发生改变
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}
  • 将View添加到DecorView的mContentParent中

在上述步骤中已经创建并初始化了DecorView,所以这步直接将Activity的视图添加到DecorView的mContentParent中,即
mLayoutInflater.inflate(layoutResID, mContentParent)。

  • 回调Activity的onContentChanged方法通知Activity视图已经发生改变

这三步后DecorView已经被创建并初始化完毕,Activity的布局文件已经成功添加到DecorView的mContentParent中,但是DecorView并没有被WindowManager正式添加到Window中。 虽然在Activity的attach方法中Window已经被创建,但是这个时候DecorView并没有被WindowManager识别,所以这时Window无法提供具体功能,无法接受外界的输入信息。在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible,这样才真正完成了添加和现实这两个过程。

    void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

2. Dialog 的 Window 创建过程

类似Activity的Window创建过程,有如下几步:

  • 创建Window

Dialog的Window同样是通过PolicyManager的makeNewWindow方法来完成的。创建后的对象实际上就是PhoneWindow

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
     if (createContextThemeWrapper) {
         if (themeResId == ResourceId.ID_NULL) {
             final TypedValue outValue = new TypedValue();
             context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
             themeResId = outValue.resourceId;
         }
         mContext = new ContextThemeWrapper(context, themeResId);
     } else {
         mContext = context;
     }

     mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

     final Window w = new PhoneWindow(mContext);
     mWindow = w;
     w.setCallback(this);
     w.setOnWindowDismissedCallback(this);
     w.setOnWindowSwipeDismissedCallback(() -> {
         if (mCancelable) {
             cancel();
         }
     });
     w.setWindowManager(mWindowManager, null, null);
     w.setGravity(Gravity.CENTER);

     mListenersHandler = new ListenersHandler(this);
 }
  • 初始化 DecorView 并将Dialog的视图添加到DecorView中

    public void setContentView(@LayoutRes int layoutResID) {
    mWindow.setContentView(layoutResID);
    }

  • 将 DecorView 并添加到Window中并显示

在Dialog的show方法中,会通过 WindowManager 将DecorView添加到Window中。

 //Start the dialog and display it on screen.  The window is placed in the
 //application layer and opaque.  Note that you should not override this
 //method to do initialization when the dialog is shown, instead implement
 //that in {@link #onStart}.


public void show() {
    if (mShowing) {
        if (mDecor != null) {
            if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
            }
            mDecor.setVisibility(View.VISIBLE);
        }
        return;
    }

    mCanceled = false;

    if (!mCreated) {
        dispatchOnCreate(null);
    } else {
        // Fill the DecorView in on any configuration changes that
        // may have occured while it was removed from the WindowManager.
        final Configuration config = mContext.getResources().getConfiguration();
        mWindow.getDecorView().dispatchConfigurationChanged(config);
    }

    onStart();
    mDecor = mWindow.getDecorView();

    if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
        final ApplicationInfo info = mContext.getApplicationInfo();
        mWindow.setDefaultIcon(info.icon);
        mWindow.setDefaultLogo(info.logo);
        mActionBar = new WindowDecorActionBar(this);
    }

    WindowManager.LayoutParams l = mWindow.getAttributes();
    if ((l.softInputMode
            & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
        WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
        nl.copyFrom(l);
        nl.softInputMode |=
                WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        l = nl;
    }

    mWindowManager.addView(mDecor, l);//通过 WindowManager 将DecorView添加到Window中
    mShowing = true;

    sendShowMessage();
}

当Dialog被关闭时,会通过WindowManager来移除DecorView,如:

mWindowManager.removeViewImmediate(mDecor);

注意:

  • 普通的Dialog有一个特殊之处,就是必须采用Activity的Context,如果采用Application的Context,就会报错

    Dialog dialog = new Dialog(this.getApplicationContext());//XXX错误,会报错
    TextView mTxt = new TextView(this);
    mTxt.setText("this is toast!");
    dialog.setContextView(mTxt);
    dialog.show();
    

提示没有应用token所导致的,而应用token一般只有Activity拥有,所以这里只需要用Activity作为Context来显示对话框即可。

  • 系统Window比较特殊,它可以不需要token,所以可以指定对话框的Window为系统类型就可以正常弹出对话框了,所以上述erro中可以指定Window类型, 即 dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR);

当然还需要在AndroidManifest文件中声明权限

3. Toast 的 Window 创建过程

由于Toast具有定时取消这一功能,所以系统采用了Handler。在Toast内部有两类IPC过程,第一类是Toast访问NotificationManagerService(简称NMS),第二类是NotificationManagerService回调Toast里面的IN接口。

Toast属于系统Window,它内部视图有两种,一种系统默认样式,一种通过setView来指定一个自定义的View,但都对应Toast的一个View类型的内部成员mNextView。 Toast提供了show和cancel方法分别用于显示和隐藏Toast,内部是一个IPC的过程

/**
 * Show the view for the specified duration.
 */
public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;//TN是一个Binder类
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);//
    } catch (RemoteException e) {
        // Empty
    }
}

/**
 * Close the view if it's showing, or don't show it if it isn't showing yet.
 * You do not normally have to call this.  Normally view will disappear on its own
 * after the appropriate duration.
 */
public void cancel() {
    mTN.cancel();
}

参考资料:《Android 开发艺术探索》
http://blog.csdn.net/yhaolpz/article/details/68936932
http://blog.csdn.net/luoshengyang/article/details/8462738
http://blog.csdn.net/innost/article/details/47660193
https://www.tuicool.com/articles/MjAjIfU
http://blog.csdn.net/wzy_1988/article/details/43341761

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值