本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
ContentProvider客户端处理provider逻辑分析
飘飘悠悠 · 169浏览 · 发布于2022-10-21 +关注

这篇文章主要为大家介绍了ContentProvider客户端处理provider逻辑分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪


引言

前面一篇文章分析了 AMS 端处理 provider 的逻辑,请读者务必仔细阅读前面一篇文章,否则看本文,你可能有很多疑惑。

以查询 provider 为例来分析客户端是如何处理 provider,它调用的是 ContentResolver#query()

// ContentResolver.java
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
        @Nullable String[] projection, @Nullable Bundle queryArgs,
        @Nullable CancellationSignal cancellationSignal) {
    Objects.requireNonNull(uri, "uri");
    // ApplicationContentResolver 的 mWrapped 为 null
    try {
        if (mWrapped != null) {
            return mWrapped.query(uri, projection, queryArgs, cancellationSignal);
        }
    } catch (RemoteException e) {
        return null;
    }
    // 1. 获取 unstable provider
    IContentProvider unstableProvider = acquireUnstableProvider(uri);
    if (unstableProvider == null) {
        return null;
    }
    IContentProvider stableProvider = null;
    Cursor qCursor = null;
    try {
        long startTime = SystemClock.uptimeMillis();
        // 获取取消操作的接口
        ICancellationSignal remoteCancellationSignal = null;
        if (cancellationSignal != null) {
            cancellationSignal.throwIfCanceled();
            remoteCancellationSignal = unstableProvider.createCancellationSignal();
            cancellationSignal.setRemote(remoteCancellationSignal);
        }
        try {
            // 2. 执行操作
            qCursor = unstableProvider.query(mContext.getAttributionSource(), uri, projection,
                    queryArgs, remoteCancellationSignal);
        } catch (DeadObjectException e) {
            // 处理 unstable provider 进程挂掉的情况
            // 通知 AMS,provider 进程挂掉了
            unstableProviderDied(unstableProvider);
            // 获取 stable provider,再次尝试获取数据
            stableProvider = acquireProvider(uri);
            if (stableProvider == null) {
                return null;
            }
            qCursor = stableProvider.query(mContext.getAttributionSource(), uri, projection,
                    queryArgs, remoteCancellationSignal);
        }
        if (qCursor == null) {
            return null;
        }
        // Force query execution.  Might fail and throw a runtime exception here.
        qCursor.getCount();
        long durationMillis = SystemClock.uptimeMillis() - startTime;
        maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);
        // 注意,这里最终还是从 stable provider 获取 provider 接口
        final IContentProvider provider = (stableProvider != null) ? stableProvider
                : acquireProvider(uri);
        final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
        stableProvider = null;
        qCursor = null;
        // 3. 返回数据
        return wrapper;
    } catch (RemoteException e) {
        return null;
    } finally {
        // ...
    }
}

纵观整个 provider 的查询过程,其实就是三步

  • 获取 provider。

  • 从获取到的 provider 执行查询操作。

  • 返回查询的结果。

我们注意到,代码中出现了两种 provider,unstable provider 和 stable provider。这两者的区别是,如果 provider 进程挂掉了,对于 stable provider,会杀死客户端进程,而 unstable 不会。这个我们会在后面分析。

现在我们要抓住重点,来分析如何获取 provider 。unstable provider 和 stable provider 的获取方式其实是一样的,本文只分析获取 unstbale provider。

1. 获取 provider

对于 app 进程来说,ContentResolver 接口的实现类为 ApplicationContentResolver,获取 unstable provider 的操作最终会调用 ApplicationContentResolver#acquireUnstableProvider()

//ContextImpl.java
class ContextImpl {
    private static final class ApplicationContentResolver extends ContentResolver {
        private final ActivityThread mMainThread;
        @Override
        protected IContentProvider acquireUnstableProvider(Context c, String auth) {
            return mMainThread.acquireProvider(c,
                    ContentProvider.getAuthorityWithoutUserId(auth),
                    resolveUserIdFromAuthority(auth), false);
        }
    }
}

原来最终是交给 ActivityThread 来获取 provider

// ActivityThread.java
public final IContentProvider acquireProvider(
        Context c, String auth, int userId, boolean stable) {
    // 从本地获取
    final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
    if (provider != null) {
        return provider;
    }
    ContentProviderHolder holder = null;
    // 合成一个 KEY
    final ProviderKey key = getGetProviderKey(auth, userId);
    try {
        synchronized (key) {
            // 1. 获取 ActivityManagerService 获取
            holder = ActivityManager.getService().getContentProvider(
                    getApplicationThread(), c.getOpPackageName(), auth, userId, stable);
            // 2. 等待 provider 发布完成
            // holder != null 表示 provider 存在
            // holder.provider == null 表示 provider 正在发布中
            // holder.mLocal 为 false,表示 provider 不是安装在客户端
            if (holder != null && holder.provider == null && !holder.mLocal) {
                synchronized (key.mLock) {
                    // 2.1 超时等等 provider 发布
                    // 超时时间一般为 20s
                    key.mLock.wait(ContentResolver.CONTENT_PROVIDER_READY_TIMEOUT_MILLIS);
                    // 这里可能因为超时被唤醒,获取的数据为空
                    // 也可以是因为provider发布完成,被AMS唤醒,holder 为AMS返回的数据
                    holder = key.mHolder;
                }
                // 2.2 确认是否是超时唤醒
                if (holder != null && holder.provider == null) {
                    // probably timed out
                    holder = null;
                }
            }
        }
    }
    // ...
    // 这里记录了获取provider失败的日志
    if (holder == null) {
        if (UserManager.get(c).isUserUnlocked(userId)) {
            Slog.e(TAG, "Failed to find provider info for " + auth);
        } else {
            Slog.w(TAG, "Failed to find provider info for " + auth + " (user not unlocked)");
        }
        return null;
    }
    // 3. 成功从服务端获取 provider,本地安装它
    holder = installProvider(c, holder, holder.info,
            true /*noisy*/, holder.noReleaseNeeded, stable);
    return holder.provider;
}

客户端获取 provider 的过程大致分为如下几步

  • 从 AMS 获取 provider。

  • 如果 provider 还是发布的过程中,那么就超时等待它发布完成。 但是等待是有时间限制的,大约为 20s。超时等待的过程中被唤醒,有两种可能,一种是因为超时了,另外一种是因为 provider 成功发布,AMS 唤醒了客户端。因此需要判断到底是哪一种情况,检测的条件是被唤醒后,是否获取到 provider binder,也就是 holder.provider。详见【1.1 等待 provider 发布】

  • 从 AMS 成功获取到 provider 后,那就在本地“安装”。这个方法的命令起的并不是很好,如果成功从 AMS 获取到 provider,其实这里的逻辑是保存数据。而如果 AMS 通知客户端,provider 可以安装在客户端进程中,客户端会在这个方法中创建 ContentProvider 对象并保存,这才叫安装。详见【1.2 安装 provider】

1.1 等待 provider 发布

从前面的文章可知,当 provider 发布超时 或者 成功发布时,都会调用 ContentProviderRecord#onProviderPublishStatusLocked(boolean status) 来通知客户端 provider 的发布状态。参数 status 如果为 true,表示发布成功,如果为 false,表示发布超时。

// ContentProviderRecord.java
void onProviderPublishStatusLocked(boolean status) {
    final int numOfConns = connections.size();
    for (int i = 0; i < numOfConns; i++) {
        // 遍历所有等待 provider 发布的客户端连接
        final ContentProviderConnection conn = connections.get(i);
        if (conn.waiting && conn.client != null) {
            final ProcessRecord client = conn.client;
            // 记录发布超时的日志
            if (!status) {
                // 从这里可以看出status为false时,不一定表示发布超时,还可能因为进程挂掉了
                if (launchingApp == null) {
                    Slog.w(TAG_AM, "Unable to launch app "
                            + appInfo.packageName + "/"
                            + appInfo.uid + " for provider "
                            + info.authority + ": launching app became null");
                    EventLogTags.writeAmProviderLostProcess(
                            UserHandle.getUserId(appInfo.uid),
                            appInfo.packageName,
                            appInfo.uid, info.authority);
                } else {
                    Slog.wtf(TAG_AM, "Timeout waiting for provider "
                            + appInfo.packageName + "/"
                            + appInfo.uid + " for provider "
                            + info.authority
                            + " caller=" + client);
                }
            }
            // 通知客户端
            final IApplicationThread thread = client.getThread();
            if (thread != null) {
                try {
                    thread.notifyContentProviderPublishStatus(
                            newHolder(status ? conn : null, false),
                            info.authority, conn.mExpectedUserId, status);
                } catch (RemoteException e) {
                }
            }
        }
        conn.waiting = false;
    }
}

很简单,通过遍历所有等待 provider 发布的客户端连接,然后通过客户端 attach 的 thread 来通知它们。

// ActivityThread.java
public void notifyContentProviderPublishStatus(@NonNull ContentProviderHolder holder,
        @NonNull String authorities, int userId, boolean published) {
    final String auths[] = authorities.split(";");
    for (String auth: auths) {
        final ProviderKey key = getGetProviderKey(auth, userId);
        synchronized (key.mLock) {
            // 保存服务端传过来的数据
            key.mHolder = holder;
            // 唤醒等待provider的线程
            key.mLock.notifyAll();
        }
    }
}

客户端收到信息后,唤醒了等待的线程,谁在等待呢?这里是不是有点熟悉,其实就是前面分析获取 provider 时,超时等待,部分代码如下

// ActivityThread.java
public final IContentProvider acquireProvider(
        Context c, String auth, int userId, boolean stable) {
    // ...
    try {
        synchronized (key) {
            holder = ActivityManager.getService().getContentProvider(
                    getApplicationThread(), c.getOpPackageName(), auth, userId, stable);
            // 等待 provider 发布完成
            if (holder != null && holder.provider == null && !holder.mLocal) {
                synchronized (key.mLock) {
                    // 超时等待
                    key.mLock.wait(ContentResolver.CONTENT_PROVIDER_READY_TIMEOUT_MILLIS);
                    // 这里可能因为超时被唤醒,获取的数据为空
                    // 也可以是因为provider发布完成,被AMS唤醒,holder 为AMS返回的数据
                    holder = key.mHolder;
                }
                // 确认是否是超时唤醒
                if (holder != null && holder.provider == null) {
                    // probably timed out
                    holder = null;
                }
            }
        }
    }
    // ...
}

超时等待 provider 发布时,如果一旦被唤醒,再次获取 key.mHolder,因为如果成功发布,holder.provider 是不为空的,因为它就是 provider binder,否则就是超时唤醒。

1.2 安装 provider

客户端如果成功从 AMS 获取到 provider,那么就会安装它,其实这里的操作是保存数据,其实最主要的就是保存 provider 接口,同时也是保存 provider binder.

private ContentProviderHolder installProvider(Context context,
        ContentProviderHolder holder, ProviderInfo info,
        boolean noisy, boolean noReleaseNeeded, boolean stable) {
    ContentProvider localProvider = null;
    IContentProvider provider;
    // 成功从 AMS 获取 provider,下面两个条件都是不成立
    if (holder == null || holder.provider == null) {
        // ...
    } else {
        // 获取 provider 接口,其实就是获取 provider binder
        provider = holder.provider;
    }
    ContentProviderHolder retHolder;
    synchronized (mProviderMap) {
        // 从 provider 接口中获取 binder 对象
        IBinder jBinder = provider.asBinder();
        if (localProvider != null) {
            // ...
        } else {
            ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
            if (prc != null) {
                // ...
            } else {
                // 1. 创建 provider 记录,并保存
                ProviderClientRecord client = installProviderAuthoritiesLocked(
                        provider, localProvider, holder);
                // persistent app 的 provider 是不需要释放的
                if (noReleaseNeeded) {
                    prc = new ProviderRefCount(holder, client, 1000, 1000);
                } else {
                    prc = stable
                            ? new ProviderRefCount(holder, client, 1, 0)
                            : new ProviderRefCount(holder, client, 0, 1);
                }
                // 2. 保存 provider 计数
                mProviderRefCountMap.put(jBinder, prc);
            }
            retHolder = prc.holder;
        }
    }
    return retHolder;
}
private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,
        ContentProvider localProvider, ContentProviderHolder holder) {
    final String auths[] = holder.info.authority.split(";");
    final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid);
    // ...
    //  创建一条 provider 记录
    final ProviderClientRecord pcr = new ProviderClientRecord(
            auths, provider, localProvider, holder);
    // 一个 ContentProvider 可以声明多个 authority
    for (String auth : auths) {
        final ProviderKey key = new ProviderKey(auth, userId);
        //  mProviderMap 保存
        final ProviderClientRecord existing = mProviderMap.get(key);
        if (existing != null) {
            Slog.w(TAG, "Content provider " + pcr.mHolder.info.name
                    + " already published as " + auth);
        } else {
            mProviderMap.put(key, pcr);
        }
    }
    return pcr;
}

很简单,就是用两个数据结构保存数据。

2. provider 实现多进程实例

前面我们总是隐隐约约地提到,provider 可以安装在客户端进程,那么什么样的条件下,provider 可以安装在客户端进程中? 前面一篇文章的分析中有提到过,现在展示出部分代码

// ContentProviderHelper.java
private ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
        String name, IBinder token, int callingUid, String callingPackage, String callingTag,
        boolean stable, int userId) {
    // ...
    synchronized (mService) {
        // 获取客户端的进程实例
        ProcessRecord r = null;
        if (caller != null) {
            r = mService.getRecordForAppLOSP(caller);
            if (r == null) {
                throw new SecurityException("Unable to find app for caller " + caller
                        + " (pid=" + Binder.getCallingPid() + ") when getting content provider "
                        + name);
            }
        }
        // ...
        // provider 正在运行
        if (providerRunning) {
            cpi = cpr.info;
            if (r != null &amp;&amp; cpr.canRunHere(r)) {
                // This provider has been published or is in the process
                // of being published...  but it is also allowed to run
                // in the caller's process, so don't make a connection
                // and just let the caller instantiate its own instance.
                ContentProviderHolder holder = cpr.newHolder(null, true);
                // don't give caller the provider object, it needs to make its own.
                holder.provider = null;
                return holder;
            }
            // ...
        }
        // provider 没有运行
        if (!providerRunning) {
            // ...
            if (r != null &amp;&amp; cpr.canRunHere(r)) {
                // If this is a multiprocess provider, then just return its
                // info and allow the caller to instantiate it.  Only do
                // this if the provider is the same user as the caller's
                // process, or can run as root (so can be in any process).
                return cpr.newHolder(null, true);
            }
            // ...
        }
        // ...
    }
    // ...
}

可以看到,无论 provider 是否已经运行,都有机会在客户端进程中创建 provider 实例,而这个机会就在 ContentProviderRecord#canRunHere()

provider 已经运行,居然还可以运行在客户端进程中,也就是在客户端进程中创建 ContentProvider 实例,这样的设计又是为了什么呢?

public boolean canRunHere(ProcessRecord app) {
    // info 为 provider 信息,也就是在 AndroidManifest 中声明的 provider 信息
    // provider 可以 运行在客户端进程中的条件
    // 1. provider 所在的 app 的 uid 与客户端 app 的 uid 相同
    // 2. provider 支持多进程 或者 provider 的进程名与客户端 app 的进程名相同
    return (info.multiprocess || info.processName.equals(app.processName))
            && uid == app.info.uid;
}

这里的条件可要看清楚了,首先 provider 所在 app 和 客户端 app 的 uid 相同,其实就是下面这个玩意要一样

<manifest
    android:sharedUserId="">

然后,还需要 provider 支持多进程,其实就是下面这个玩意

<provider
    android:multiprocess="true"/>

如果 provider 不支持多进程,只要 provider 的进程名与客户端 app 的进程名一样,provider 也是可以运行在客户端进程中。那么 provider 进程名是什么呢? provider 可以声明自己的进程名,如下

<provider
    android:process=""
   />

而如果 provider 没有声明自己的进程名,那么 provider 进程名取自 app 的进程名。

现在 provider 怎样运行在客户端进程中,大家会玩了吗?如果会玩了,那么继续看下客户端如何安装 provider,这一次可就是真的安装 provider 了

private ContentProviderHolder installProvider(Context context,
        ContentProviderHolder holder, ProviderInfo info,
        boolean noisy, boolean noReleaseNeeded, boolean stable) {
    ContentProvider localProvider = null;
    IContentProvider provider;
    // 此时 holder.provider == null 是成立的
    if (holder == null || holder.provider == null) {
        // ...
        try {
            final java.lang.ClassLoader cl = c.getClassLoader();
            LoadedApk packageInfo = peekPackageInfo(ai.packageName, true);
            if (packageInfo == null) {
                // System startup case.
                packageInfo = getSystemContext().mPackageInfo;
            }
            // 1. 通过反射创建 ContentProvider 对象
            localProvider = packageInfo.getAppFactory()
                    .instantiateProvider(cl, info.name);
            // 获取 provider 接口,其实就是获取 provider binder
            provider = localProvider.getIContentProvider();
            if (provider == null) {
                return null;
            }
            // 2. 为 ContentProvider 对象保存 provider 信息,并且调用 ContentProvider#onCreate()
            localProvider.attachInfo(c, info);
        } catch (java.lang.Exception e) {
            // ...
        }
    } else {
        // ...
    }
    ContentProviderHolder retHolder;
    synchronized (mProviderMap) {
        if (DEBUG_PROVIDER) Slog.v(TAG, "Checking to add " + provider
                + " / " + info.name);
        IBinder jBinder = provider.asBinder();
        if (localProvider != null) {
            ComponentName cname = new ComponentName(info.packageName, info.name);
            ProviderClientRecord pr = mLocalProvidersByName.get(cname);
            if (pr != null) {
                // ...
            } else {
                // 本地创建 ContentProviderHolder
                holder = new ContentProviderHolder(info);
                // 保存 provider binder
                holder.provider = provider;
                // 本地安装的 provider,不需要释放
                holder.noReleaseNeeded = true;
                // 3. 创建 provider 记录,并保存
                pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
                mLocalProviders.put(jBinder, pr);
                mLocalProvidersByName.put(cname, pr);
            }
            retHolder = pr.mHolder;
        } else {
            // ...
        }
    }
    return retHolder;
}

其实这一部分代码在前面文章中已经分析过,这里简单介绍下过程

  • 客户端自己创建 ContentProvider 对象,然后保存 provider 信息,并调用 ContentProvider#onCreate() 方法。

  • 创建 provider 记录,也就是 ContentProviderRecord 对象,然后用数据结构保存。

3. 两种 provider 区别

前面我们提到过 unstable provider 和 stable provider 的区别,现在我们用代码来解释下这两者的区别

假设我们通过 ActivityManager#forceStopPackage() 来杀掉 provider 进程,在 AMS 的调用如下

public void forceStopPackage(final String packageName, int userId) {
    // ...
    try {
        IPackageManager pm = AppGlobals.getPackageManager();
        synchronized(this) {
            int[] users = userId == UserHandle.USER_ALL
                    ? mUserController.getUsers() : new int[] { userId };
            for (int user : users) {
                // ...
                if (mUserController.isUserRunning(user, 0)) {
                    // 杀掉进程
                    forceStopPackageLocked(packageName, pkgUid, "from pid " + callingPid);
                    // 发送广播
                    finishForceStopPackageLocked(packageName, pkgUid);
                }
            }
        }
    } finally {
        Binder.restoreCallingIdentity(callingId);
    }
}

最终调用如下代码

final boolean forceStopPackageLocked(String packageName, int appId,
        boolean callerWillRestart, boolean purgeCache, boolean doit,
        boolean evenPersistent, boolean uninstalling, int userId, String reason) {
    // ...
    // 获取 app 的所有 provider
    ArrayList<ContentProviderRecord> providers = new ArrayList<>();
    if (mCpHelper.getProviderMap().collectPackageProvidersLocked(packageName, null, doit,
            evenPersistent, userId, providers)) {
        if (!doit) {
            return true;
        }
        didSomething = true;
    }
    // 移除 provider
    for (i = providers.size() - 1; i >= 0; i--) {
        mCpHelper.removeDyingProviderLocked(null, providers.get(i), true);
    }
    // ...
}

不出意外,最终由 ContentProviderHelper 来移除 provider

boolean removeDyingProviderLocked(ProcessRecord proc, ContentProviderRecord cpr,
        boolean always) {
    // ...
    for (int i = cpr.connections.size() - 1; i >= 0; i--) {
        ContentProviderConnection conn = cpr.connections.get(i);
        // ...
        ProcessRecord capp = conn.client;
        final IApplicationThread thread = capp.getThread();
        conn.dead = true;
        // 1. 如有 stable provider 的客户端
        if (conn.stableCount() > 0) {
            final int pid = capp.getPid();
            // 注意,要排除 persistent app 进程,以及 system_server 进程
            if (!capp.isPersistent() && thread != null
                    && pid != 0 && pid != ActivityManagerService.MY_PID) {
                // 杀掉客户端进程
                capp.killLocked(
                        "depends on provider " + cpr.name.flattenToShortString()
                        + " in dying proc " + (proc != null ? proc.processName : "??")
                        + " (adj " + (proc != null ? proc.mState.getSetAdj() : "??") + ")",
                        ApplicationExitInfo.REASON_DEPENDENCY_DIED,
                        ApplicationExitInfo.SUBREASON_UNKNOWN,
                        true);
            }
        }
        // 2. 如果只有 unstable provider 客户端
        else if (thread != null && conn.provider.provider != null) {
            try {
                // 通知客户端移除数据
                thread.unstableProviderDied(conn.provider.provider.asBinder());
            } catch (RemoteException e) {
            }
            // In the protocol here, we don't expect the client to correctly
            // clean up this connection, we'll just remove it.
            cpr.connections.remove(i);
            if (conn.client.mProviders.removeProviderConnection(conn)) {
                mService.stopAssociationLocked(capp.uid, capp.processName,
                        cpr.uid, cpr.appInfo.longVersionCode, cpr.name, cpr.info.processName);
            }
        }
    }
    // ...
}

看到了吧,对于 stable provider,如果 provider 进程挂掉了,那么客户端也会受牵连被杀掉。

而对于 unstable provider,如果 provier 进程挂掉了,客户端只是移除了保存了的数据而已,并不会被杀掉。

最后,我们再来看看文章开头获取 provider 时关于两种 provider 代码

// ContentResolver.java
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
        @Nullable String[] projection, @Nullable Bundle queryArgs,
        @Nullable CancellationSignal cancellationSignal) {
    // ...
    // 获取 unstable provider
    IContentProvider unstableProvider = acquireUnstableProvider(uri);
    if (unstableProvider == null) {
        return null;
    }
    IContentProvider stableProvider = null;
    Cursor qCursor = null;
    try {
        // ...
        // 注意,这里获取的 stable provider 并返回
        final IContentProvider provider = (stableProvider != null) ? stableProvider
                : acquireProvider(uri);
        final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
        stableProvider = null;
        qCursor = null;
        // 返回数据
        return wrapper;
    } catch (RemoteException e) {
        return null;
    } finally {
        // ...
    }
}

我们可以看到,查询的时候使用的是 unstable provier,但是返回的结果 Curosr 使用的是 stable provider。这说明了什么? 它说明了,在 Cursor 没有被 close 之前,只要 provider 进程挂掉了,那么客户端也会受牵连,会被杀掉。


相关推荐

android下vulkan与opengles纹理互通

talkchan · 1173浏览 · 2020-11-23 10:37:39
Android 使用RecyclerView实现轮播图

奔跑的男人 · 2171浏览 · 2019-05-09 17:11:13
微软发布新命令行工具 Windows Terminal

吴振华 · 866浏览 · 2019-05-09 17:15:04
Facebook 停止屏蔽部分区块链广告

· 752浏览 · 2019-05-09 17:20:08
加载中

0评论

评论
分类专栏
小鸟云服务器
扫码进入手机网页