ContentProvider的基本使用


理解 ContentProvider 原理(一)





1.ContentResolver如何应用

public void testQuery() {
    Uri uri = Uri.parse("content://cn.xyCompany.providers.personProvider/person/19");
    ContentResolver resolver = this.getContext().getContentResolver();
    Cursor cursor = resolver.query(uri, new String[]{"id","name","phone"}, null, null, "id asc");
    if(cursor.moveToFirst()) {
        Log.i("query", cursor.getString(cursor.getColumnIndex("name")));
    }
    cursor.close();
}复制代码

2.ContentResolver是个啥


无论Activity还是应用Context获取ContextResolver,最终都调用了ContextImpl.getContentResolver,

806    @Override
807    public ContentResolver getContentResolver() {
808        return mContentResolver;
809    }复制代码

是在什么时候构造的呢

2224    private ContextImpl(ContextImpl container, ActivityThread mainThread,
2225            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
2226            Display display, Configuration overrideConfiguration) {
2227        mOuterContext = this;
2228
2229        mMainThread = mainThread;
2230        mActivityToken = activityToken;
2231        mRestricted = restricted;
2232
2233        if (user == null) {
2234            user = Process.myUserHandle();
2235        }
2236        mUser = user;
2237
2238        mPackageInfo = packageInfo;
2239        mResourcesManager = ResourcesManager.getInstance();
2250        }
2251        mDisplayAdjustments.setCompatibilityInfo(compatInfo);
2252        mDisplayAdjustments.setActivityToken(activityToken);
2253
2254        Resources resources = packageInfo.getResources(mainThread);
2267        mResources = resources;
2286        mContentResolver = new ApplicationContentResolver(this, mainThread, user);
2287    }复制代码
上面最后一行代码,创建了一个ApplicationContentResolver

2407    private static final class ApplicationContentResolver extends ContentResolver {
2408        private final ActivityThread mMainThread;
2409        private final UserHandle mUser;
2410
2411        public ApplicationContentResolver(
2412                Context context, ActivityThread mainThread, UserHandle user) {
2413            super(context);
2414            mMainThread = Preconditions.checkNotNull(mainThread);
2415            mUser = Preconditions.checkNotNull(user);
2416        }
2417
2418        @Override
2419        protected IContentProvider acquireProvider(Context context, String auth) {
2420            return mMainThread.acquireProvider(context,
2421                    ContentProvider.getAuthorityWithoutUserId(auth),
2422                    resolveUserIdFromAuthority(auth), true);
2423        }
2424
2425        @Override
2426        protected IContentProvider acquireExistingProvider(Context context, String auth) {
2427            return mMainThread.acquireExistingProvider(context,
2428                    ContentProvider.getAuthorityWithoutUserId(auth),
2429                    resolveUserIdFromAuthority(auth), true);
2430        }
2431
2432        @Override
2433        public boolean releaseProvider(IContentProvider provider) {
2434            return mMainThread.releaseProvider(provider, true);
2435        }
2436
2437        @Override
2438        protected IContentProvider acquireUnstableProvider(Context c, String auth) {
2439            return mMainThread.acquireProvider(c,
2440                    ContentProvider.getAuthorityWithoutUserId(auth),
2441                    resolveUserIdFromAuthority(auth), false);
2442        }
2443
2444        @Override
2445        public boolean releaseUnstableProvider(IContentProvider icp) {
2446            return mMainThread.releaseProvider(icp, false);
2447        }
2448
2449        @Override
2450        public void unstableProviderDied(IContentProvider icp) {
2451            mMainThread.handleUnstableProviderDied(icp.asBinder(), true);
2452        }
2453
2454        @Override
2455        public void appNotRespondingViaProvider(IContentProvider icp) {
2456            mMainThread.appNotRespondingViaProvider(icp.asBinder());
2457        }
2458
2459        /** @hide */
2460        protected int resolveUserIdFromAuthority(String auth) {
2461            return ContentProvider.getUserIdFromAuthority(auth, mUser.getIdentifier());
2462        }
2463    }
2464}复制代码

似乎也没有啥东西,传入了ContextImpl本身以及ActivityThread,单得注意它继承自ContentResolver. 看一下代码也有2000行,看看关键函数吧。一个构造函数没做啥,几个abstract函数留给ApplicationContentResolver实现。看样子似乎把Provider分成了Stable的和unStable的

282    public ContentResolver(Context context) {
283        mContext = context != null ? context : ActivityThread.currentApplication();
284        mPackageName = mContext.getOpPackageName();
285    }
286
287    /** @hide */
288    protected abstract IContentProvider acquireProvider(Context c, String name);
289
290    /**
291     * Providing a default implementation of this, to avoid having to change a
292     * lot of other things, but implementations of ContentResolver should
293     * implement it.
294     *
295     * @hide
296     */
297    protected IContentProvider acquireExistingProvider(Context c, String name) {
298        return acquireProvider(c, name);
299    }
300
301    /** @hide */
302    public abstract boolean releaseProvider(IContentProvider icp);
303    /** @hide */
304    protected abstract IContentProvider acquireUnstableProvider(Context c, String name);
305    /** @hide */
306    public abstract boolean releaseUnstableProvider(IContentProvider icp);
307    /** @hide */
308    public abstract void unstableProviderDied(IContentProvider icp);复制代码

其他函数可以不看,暂时关注一下query方法和call方法吧。先看看query,五个参数看看注释很明了,就第二个参数名字有点费解。

420    public final Cursor query(Uri uri, String[] projection,
421            String selection, String[] selectionArgs, String sortOrder) {
422        return query(uri, projection, selection, selectionArgs, sortOrder, null);
423    }复制代码
这里的query方法首先调用abstract方法acquireUnStableProvider获取IContentProvider unstableProvider,调用其query方法。如果出现异常,那么再利用 acquireProvider获得稳定的 IContentProvider stableProvider,调用其query方法。不管哪种选择,最终都会调用release方法释放IContentProvider。

459    public final Cursor query(final Uri uri, String[] projection,
460            String selection, String[] selectionArgs, String sortOrder,
461            CancellationSignal cancellationSignal) {
462        IContentProvider unstableProvider = acquireUnstableProvider(uri);
463        if (unstableProvider == null) {
464            return null;
465        }
466        IContentProvider stableProvider = null;
467        Cursor qCursor = null;
468        try {
469            long startTime = SystemClock.uptimeMillis();
470
477            try {
478                qCursor = unstableProvider.query(mPackageName, uri, projection,
479                        selection, selectionArgs, sortOrder, remoteCancellationSignal);
480            } catch (DeadObjectException e) {
484                unstableProviderDied(unstableProvider);
485                stableProvider = acquireProvider(uri);
486                if (stableProvider == null) {
487                    return null;
488                }
489                qCursor = stableProvider.query(mPackageName, uri, projection,
490                        selection, selectionArgs, sortOrder, remoteCancellationSignal);
491            }
492            if (qCursor == null) {
493                return null;
494            }
495
497            qCursor.getCount();
498            long durationMillis = SystemClock.uptimeMillis() - startTime;
500
501            // Wrap the cursor object into CursorWrapperInner object.
502            CursorWrapperInner wrapper = new CursorWrapperInner(qCursor,
503                    stableProvider != null ? stableProvider : acquireProvider(uri));
504            stableProvider = null;
505            qCursor = null;
506            return wrapper;
507        } catch (RemoteException e) {
510            return null;
511        } finally {
512            if (qCursor != null) {
513                qCursor.close();
514            }
515            if (cancellationSignal != null) {
516                cancellationSignal.setRemote(null);
517            }
518            if (unstableProvider != null) {
519                releaseUnstableProvider(unstableProvider);
520            }
521            if (stableProvider != null) {
522                releaseProvider(stableProvider);
523            }
524        }
525    }复制代码

我们接下来要看看call方法,但是这里已经很好奇了,Provider基本会是extends Binder implements IContentProvider吧,先查源代码发现并不是呀,就看看谁实现了IContentProvider呗,后面再分析Provider吧,Provider使用了一个Transport对象,就是ContentProviderBinder。

好了继续看call方法,非常纯净!获取了稳定的IContentProvider,直接调用call。

1360    public final Bundle call(Uri uri, String method, String arg, Bundle extras) {
1361        if (uri == null) {
1362            throw new NullPointerException("uri == null");
1363        }
1364        if (method == null) {
1365            throw new NullPointerException("method == null");
1366        }
1367        IContentProvider provider = acquireProvider(uri);
1368        if (provider == null) {
1369            throw new IllegalArgumentException("Unknown URI " + uri);
1370        }
1371        try {
1372            return provider.call(mPackageName, method, arg, extras);
1373        } catch (RemoteException e) {
1374            // Arbitrary and not worth documenting, as Activity
1375            // Manager will kill this process shortly anyway.
1376            return null;
1377        } finally {
1378            releaseProvider(provider);
1379        }
1380    }复制代码

先画一条分割线吧,很想看看ContentProvider类是啥样!看完接着分析ApplicationContentResolver
------------------------------------------------------------------------------------------------

主要的接口是IContentProvider,只不过实现的Native类为ContentProviderNative,而Proxy类为ContentProviderProxy。ContentProvider与ContentProviderNative不是继承关系,而是组合关系,它其中包含了一个类型为ContentProviderNative的成员变量mTranspot。那么后续,这个mTranspot肯定会发送到AMS中,保持为ContentProviderProxy,当Client需要时AMS会发送给Client。

------------------------------------------------------------------------------------------------


下面回到ApplicationContentResolver中,该调用ActivityThread的 acquireProvider获取IContentProvider,然后调用query方法和call方法了。

先把call方法翻译一下:

1360    public final Bundle call(Uri uri, String method, String arg, Bundle extras) {
1367        IContentProvider provider = acquireProvider(uri);
1371        try {
1372            return provider.call(mPackageName, method, arg, extras);
1373        } catch (RemoteException e) {
1376            return null;
1377        } finally {
1378            releaseProvider(provider);
1379        }
1380    }复制代码

1.首先利用在ActivityThread中调用ActivityManagerNative.getDefault().getContentProvider,最终可以得到一个IContentProvider

2.其次支持使用这个IContentProvider.call
3.最后调用ActivityManagerNative.getDefault().refContentProvider来释放IContentProvider引用

从这里可以看出ApplicationContentResolver只是IContentProvider的一个外观类,使用步骤:获取、使用、释放。下面关注一下获取的过程。
------------------------------------------------------------------------------------------------

首先从缓存mProviderMap中获取IContentProvider,如果缓存命中则返回,如果没有命中则通过ActivityManagerNative来获取holder并保持到mProviderMap中

4574    public final IContentProvider acquireProvider(
4575            Context c, String auth, int userId, boolean stable) {
4576        final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
4577        if (provider != null) {
4578            return provider;
4579        }
4580
4587        IActivityManager.ContentProviderHolder holder = null;
4588        try {
4589            holder = ActivityManagerNative.getDefault().getContentProvider(
4590                    getApplicationThread(), auth, userId, stable);
4591        } catch (RemoteException ex) {
4592        }
4593        if (holder == null) {
4594            Slog.e(TAG, "Failed to find provider info for " + auth);
4595            return null;
4596        }
4600        holder = installProvider(c, holder, holder.info,
4601                true /*noisy*/, holder.noReleaseNeeded, stable);
4602        return holder.provider;
4603    }

9204    private final ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
9205            String name, IBinder token, boolean stable, int userId) {
9206        ContentProviderRecord cpr;
9207        ContentProviderConnection conn = null;
9208        ProviderInfo cpi = null;
9209
9210        synchronized(this) {
9247
9248            boolean providerRunning = cpr != null;
9249            if (providerRunning) {
9250                cpi = cpr.info;
9251                String msg;
9259                if (r != null && cpr.canRunHere(r)) {
9267                    holder.provider = null;
9268                    return holder;
9269                }
9332            boolean singleton;
9430                // This is single process, and our app is now connecting to it.
9431                // See if we are already in the process of launching this
9432                // provider.
9433                final int N = mLaunchingProviders.size();
9434                int i;
9435                for (i=0; i<N; i++) {
9436                    if (mLaunchingProviders.get(i) == cpr) {
9437                        break;
9438                    }
9439                }
9440
9441                // If the provider is not already being launched, then get it
9442                // started.
9443                if (i >= N) {
9444                    final long origId = Binder.clearCallingIdentity();
9445
9446                    try {
9461                        ProcessRecord proc = getProcessRecordLocked(
9462                                cpi.processName, cpr.appInfo.uid, false);
9463                        if (proc != null && proc.thread != null) {
9468                            proc.pubProviders.put(cpi.name, cpr);
9469                            try {
9470                                proc.thread.scheduleInstallProvider(cpi);
9471                            } catch (RemoteException e) {
9472                            }
9473                        } else {
9474                            checkTime(startTime, "getContentProviderImpl: before start process");
9475                            proc = startProcessLocked(cpi.processName,
9476                                    cpr.appInfo, false, 0, "content provider",
9477                                    new ComponentName(cpi.applicationInfo.packageName,
9478                                            cpi.name), false, false, false);
9480                            if (proc == null) {
9485                                return null;
9486                            }
9487                        }
9488                        cpr.launchingApp = proc;
9489                        mLaunchingProviders.add(cpr);
9490                    } finally {
9491                        Binder.restoreCallingIdentity(origId);
9492                    }
9493                }
9508            }
9510        }
9511
9513        synchronized (cpr) {
9514            while (cpr.provider == null) {
9515                if (cpr.launchingApp == null) {
9524                    return null;
9525                }
9526                try {
9531                    if (conn != null) {
9532                        conn.waiting = true;
9533                    }
9534                    cpr.wait();
9535                } catch (InterruptedException ex) {
9536                } finally {
9537                    if (conn != null) {
9538                        conn.waiting = false;
9539                    }
9540                }
9541            }
9542        }
9543        return cpr != null ? cpr.newHolder(conn) : null;
9544    }
2515    public void handleInstallProvider(ProviderInfo info) {
2516        installContentProviders(mInitialApplication, Lists.newArrayList(info));
2517    }复制代码
1.上面的代码首先看AMS的缓存中有没有IContentProvider,有则返回,否则进入2

2.如果进程没有启动则启动进程,否则进入3

3.调用thread.scheuleInstallProvider

这里很奇怪,为什么会有步骤三呢?应用进程启动的时候在handleBindApplication中,不是已经安装了所有的Provider组件吗?莫非是中途被干掉了,比如禁用了组件!


4545    private void installContentProviders(
4546            Context context, List<ProviderInfo> providers) {
4547        final ArrayList<IActivityManager.ContentProviderHolder> results =
4548            new ArrayList<IActivityManager.ContentProviderHolder>();
4549
4550        for (ProviderInfo cpi : providers) {
4551            if (DEBUG_PROVIDER) {
4552                StringBuilder buf = new StringBuilder(128);
4553                buf.append("Pub ");
4554                buf.append(cpi.authority);
4555                buf.append(": ");
4556                buf.append(cpi.name);
4557                Log.i(TAG, buf.toString());
4558            }
4559            IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi,
4560                    false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
4561            if (cph != null) {
4562                cph.noReleaseNeeded = true;
4563                results.add(cph);
4564            }
4565        }
4566
4567        try {
4568            ActivityManagerNative.getDefault().publishContentProviders(
4569                getApplicationThread(), results);
4570        } catch (RemoteException ex) {
4571        }
4572    }复制代码
上面的代码就是Provider初始化代码,首先初始化类调用onCreate函数,缓存到mProviderMap中,通知AMS本应用的Providers都准备好了,其他应用可以使用了。

4918    private IActivityManager.ContentProviderHolder installProvider(Context context,
4919            IActivityManager.ContentProviderHolder holder, ProviderInfo info,
4920            boolean noisy, boolean noReleaseNeeded, boolean stable) {
4921        ContentProvider localProvider = null;
4922        IContentProvider provider;
4923        if (holder == null || holder.provider == null) {
4924            if (DEBUG_PROVIDER || noisy) {
4925                Slog.d(TAG, "Loading provider " + info.authority + ": "
4926                        + info.name);
4927            }
4928            Context c = null;
4929            ApplicationInfo ai = info.applicationInfo;
4950            try {
4951                final java.lang.ClassLoader cl = c.getClassLoader();
4952                localProvider = (ContentProvider)cl.
4953                    loadClass(info.name).newInstance();
4954                provider = localProvider.getIContentProvider();
4955                if (provider == null) {
4959                    return null;
4960                }
4964                localProvider.attachInfo(c, info);
4965            } catch (java.lang.Exception e) {
4971                return null;
4972            }
4973        } else {
4974            provider = holder.provider;
4977        }
4978
4979        IActivityManager.ContentProviderHolder retHolder;
4980
4981        synchronized (mProviderMap) {
4984            IBinder jBinder = provider.asBinder();
4985            if (localProvider != null) {
4986                ComponentName cname = new ComponentName(info.packageName, info.name);
4987                ProviderClientRecord pr = mLocalProvidersByName.get(cname);
4988                if (pr != null) {
4993                    provider = pr.mProvider;
4994                } else {
4995                    holder = new IActivityManager.ContentProviderHolder(info);
4996                    holder.provider = provider;
4997                    holder.noReleaseNeeded = true;
4998                    pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
4999                    mLocalProviders.put(jBinder, pr);
5000                    mLocalProvidersByName.put(cname, pr);
5001                }
5002                retHolder = pr.mHolder;
5003            } else {
5004                ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
5005                if (prc != null) {
5013                    if (!noReleaseNeeded) {
5014                        incProviderRefLocked(prc, stable);
5015                        try {
5016                            ActivityManagerNative.getDefault().removeContentProvider(
5017                                    holder.connection, stable);
5018                        } catch (RemoteException e) {
5020                        }
5021                    }
5022                } else {
5023                    ProviderClientRecord client = installProviderAuthoritiesLocked(
5024                            provider, localProvider, holder);
5025                    if (noReleaseNeeded) {
5026                        prc = new ProviderRefCount(holder, client, 1000, 1000);
5027                    } else {
5028                        prc = stable
5029                                ? new ProviderRefCount(holder, client, 1, 0)
5030                                : new ProviderRefCount(holder, client, 0, 1);
5031                    }
5032                    mProviderRefCountMap.put(jBinder, prc);
5033                }
5034                retHolder = prc.holder;
5035            }
5036        }
5037
5038        return retHolder;
5039    }复制代码
具体的installProvider函数,上面已经说过了,只不过provider是放在holder中的,holder会存到mProviderMap中。mProviderMap在下面的函数中,接着看。


4884    private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,
4885            ContentProvider localProvider, IActivityManager.ContentProviderHolder holder) {
4886        final String auths[] = PATTERN_SEMICOLON.split(holder.info.authority);
4887        final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid);
4888
4889        final ProviderClientRecord pcr = new ProviderClientRecord(
4890                auths, provider, localProvider, holder);
4891        for (String auth : auths) {
4892            final ProviderKey key = new ProviderKey(auth, userId);
4893            final ProviderClientRecord existing = mProviderMap.get(key);
4894            if (existing != null) {
4897            } else {
4898                mProviderMap.put(key, pcr);
4899            }
4900        }
4901        return pcr;
4902    }复制代码
mProviderMap是用auth和userId作为Key来存储的,ProviderKey的equals方法被改写了。


------------------------------------------------------------------------------------------------

4254    private void handleBindApplication(AppBindData data) {
4255        mBoundApplication = data;
4328
4329        final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
4343
4483

4487        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
4488        try {
4491            Application app = data.info.makeApplication(data.restrictedBackupMode, null);
4492            mInitialApplication = app;
4493
4496            if (!data.restrictedBackupMode) {
4497                List<ProviderInfo> providers = data.providers;
4498                if (providers != null) {
4499                    installContentProviders(app, providers);
4502                    mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
4503                }
4504            }
4505
4506            // Do this after providers, since instrumentation tests generally start their
4507            // test thread at this point, and we don't want that racing.
4508            try {
4509                mInstrumentation.onCreate(data.instrumentationArgs);
4510            }
4511            catch (Exception e) {
4515            }
4516
4517            try {
4518                mInstrumentation.callApplicationOnCreate(app);
4519            } catch (Exception e) {
4525            }
4526        } finally {
4527            StrictMode.setThreadPolicy(savedPolicy);
4528        }
4529    }
复制代码

本应用的Provider是在应用启动bindApplication的时候installProviders的,而应用外表的Provider是在acquireProvider的时候获得holder的进行installProvider的。
1.本应用的,启动时installProviders
2.应用外部的,在ProviderResolver远程调用时ActivityThread.acquireProvider时获取得

------------------------------------------------------------------------------------------------

9643    public final void publishContentProviders(IApplicationThread caller,
9644            List<ContentProviderHolder> providers) {

9650        synchronized (this) {
9651            final ProcessRecord r = getRecordForAppLocked(caller);
9660
9661            final long origId = Binder.clearCallingIdentity();
9662
9663            final int N = providers.size();
9664            for (int i=0; i<N; i++) {
9665                ContentProviderHolder src = providers.get(i);
9666                if (src == null || src.info == null || src.provider == null) {
9667                    continue;
9668                }
9669                ContentProviderRecord dst = r.pubProviders.get(src.info.name);
9672                if (dst != null) {
9673                    ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name);
9674                    mProviderMap.putProviderByClass(comp, dst);
9675                    String names[] = dst.info.authority.split(";");
9676                    for (int j = 0; j < names.length; j++) {
9677                        mProviderMap.putProviderByName(names[j], dst);
9678                    }
9679
9680                    int NL = mLaunchingProviders.size();
9681                    int j;
9682                    for (j=0; j<NL; j++) {
9683                        if (mLaunchingProviders.get(j) == dst) {
9684                            mLaunchingProviders.remove(j);
9685                            j--;
9686                            NL--;
9687                        }
9688                    }
9689                    synchronized (dst) {
9690                        dst.provider = src.provider;
9691                        dst.proc = r;
9692                        dst.notifyAll();
9693                    }
9694                    updateOomAdjLocked(r);
9695                }
9696            }
9697
9698            Binder.restoreCallingIdentity(origId);
9699        }
9700    }复制代码
应用的发布过程,即通知AMS已经准备好了Provider,其他应用可以取IContentProvider了

------------------------------------------------------------------------------------------------


上面的代码分析,后续再记录一下。
ContextImpl.ApplicationContentProvider.query
ActivityThread.acquireProvider
ActivityManagerService.getContentProvider
ContentProviderRecord.wait


1.thread.scheduleInstallProvider
ActivityThread.installContentProviders
provide = class.loadClass(provideInfo.name)
provide.attachInfo(providerInfo)
AMS.publishContentProviders


2.startProcessLocked
ActivityThread.main
AMS.attachApplication(ActivityThread)
thread.scheduleBindApplication
ActivityThread.handleBindApplication
ActivityThread.installContentProviders
provide = class.loadClass(provideInfo.name)
provide.attachInfo(providerInfo)
AMS.publishContentProviders
------------------------------------------------------------------------------------------------
unStable和stable的区别在于,unStable的调用在发送DeadObjectException的时候,会重新触发一次启动Provider的操作,会让进程重启,所以使用unStable方式是说要防止Provider进程不稳定被杀死。

------------------------------------------------------------------------------------------------

为什么ContentProviderNative传递到另外一个进程中,最后变成了ContentProviderProxy呢?这其实很简单,首先遵循基本的Binder机制,ContentProviderNative在natvie层表述为BBinder,透过Binder驱动传递到另外一个进程后,会变成BpBinder,如果是Java层接收在Parcel中会将其转化为BindProxy的Java对象。最后上层使用了ContentProviderHolder序列化数据,在接收到BindProxy后会调用ContentProviderNative.asInterface(IBinder)静态函数,看看代码就会明白了了,new ContentProviderProxy(new BindProxy(BpBinder(handle))).  这行代码挺带感的,顺便再写一句吧new ContentProviderNative().mObject=new JavaBBinderHolder().mObject=new JavaBBinder(ContentProviderNative)。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值