Android Hook Activity 的几种姿势

首先,我们先来看一下 startActivityForResult 方法,当 mParent 为 null 的时候,会调用到 mInstrumentation.execStartActivity 方法。当 mParent 不为 null 时,都会调用到 mParent.startActivityFromChild 方法。而 mParent 为 Activity 实例,接下来我们一起看一下 startActivityFromChild 方法。

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,

int requestCode, @Nullable Bundle options) {

options = transferSpringboardActivityOptions(options);

Instrumentation.ActivityResult ar =

mInstrumentation.execStartActivity(

this, mMainThread.getApplicationThread(), mToken, child,

intent, requestCode, options);

if (ar != null) {

mMainThread.sendActivityResult(

mToken, child.mEmbeddedID, requestCode,

ar.getResultCode(), ar.getResultData());

}

cancelInputsAndStartExitTransition(options);

}

可以看到 startActivityFromChild 中也会调用 mInstrumentation.execStartActivity 方法。因此,即我们通过 Activity startActivity 的方法启动 activity,最终都会调用到 mInstrumentation.execStartActivity 方法。因此,如果我们想要拦截的话,可以 hook 住 mInstrumentation。

由于 mInstrumentation 是类,不是 interface,不能使用动态代理的方式,因此,这里我们使用静态代理的方式。

下面让我们一起看一下 怎样 hook activity 的 mInstrumentation

  • 第一步:拿到当前 activity 的 mInstrumentation

  • 第二步:创建代理对象

  • 第三步:将我们的代理替换原 activity 的 mInstrumentation

public static void replaceInstrumentation(Activity activity) throws Exception {

Class<?> k = Activity.class;

//通过Activity.class 拿到 mInstrumentation字段

Field field = k.getDeclaredField(“mInstrumentation”);

field.setAccessible(true);

//根据activity内mInstrumentation字段 获取Instrumentation对象

Instrumentation instrumentation = (Instrumentation) field.get(activity);

//创建代理对象

Instrumentation instrumentationProxy = new ActivityProxyInstrumentation(instrumentation);

//进行替换

field.set(activity, instrumentationProxy);

}

public class ActivityProxyInstrumentation extends Instrumentation {

private static final String TAG = “ActivityProxyInstrumentation”;

// ActivityThread中原始的对象, 保存起来

Instrumentation mBase;

public ActivityProxyInstrumentation(Instrumentation base) {

mBase = base;

}

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, Activity target,

Intent intent, int requestCode, Bundle options) {

// Hook之前, 可以输出你想要的!

Log.d(TAG,"xxxx: 执行了startActivity, 参数如下: " + “who = [” + who + "], " +

“contextThread = [” + contextThread + “], token = [” + token + "], " +

“target = [” + target + “], intent = [” + intent +

“], requestCode = [” + requestCode + “], options = [” + options + “]”);

// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.

// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法

try {

Method execStartActivity = Instrumentation.class.getDeclaredMethod(

“execStartActivity”,

Context.class, IBinder.class, IBinder.class, Activity.class,

Intent.class, int.class, Bundle.class);

execStartActivity.setAccessible(true);

return (ActivityResult) execStartActivity.invoke(mBase, who,

contextThread, token, target, intent, requestCode, options);

} catch (Exception e) {

// rom修改了 需要手动适配

throw new RuntimeException(“do not support!!! pls adapt it”);

}

}

}

在 ActivityProxyInstrumentation 里面,我们打印相应的 log。

运行以下测试代码

try {

HookHelper.replaceInstrumentation(this);

} catch (Exception e) {

e.printStackTrace();

}

startActivity(new Intent(this,TestActivityStart.class));

将会看到输出以下 log

hook activity 的第二种方法

我们先来看一下 getApplicationContext startActivity 的调用关系

因此,这里我们要 hook 的是 ActivityThread 的 mInstrumentation

public static void attachContext() throws Exception {

Log.i(TAG, "attachContext: ");

// 先获取到当前的ActivityThread对象

Class<?> activityThreadClass = Class.forName(“android.app.ActivityThread”);

Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(“currentActivityThread”);

currentActivityThreadMethod.setAccessible(true);

//currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数

Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 拿到原始的 mInstrumentation字段

Field mInstrumentationField = activityThreadClass.getDeclaredField(“mInstrumentation”);

mInstrumentationField.setAccessible(true);

Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

// 创建代理对象

Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);

// 偷梁换柱

mInstrumentationField.set(currentActivityThread, evilInstrumentation);

}

public class ApplicationInstrumentation extends Instrumentation {

private static final String TAG = “ApplicationInstrumentation”;

// ActivityThread中原始的对象, 保存起来

Instrumentation mBase;

public ApplicationInstrumentation(Instrumentation base) {

mBase = base;

}

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token,

Activity target, Intent intent, int requestCode,

Bundle options) {

// Hook之前, 可以输出你想要的!

Log.d(TAG, "xxxx: 执行了startActivity, 参数如下: " + “who = [” + who + "], " + "contextThread = " +

“” + “” + “[” + contextThread + “], token = [” + token + "], " + “target = [” +

target + “], intent = [” + intent + “], requestCode = [” + requestCode + "], " +

"options = " + “[” + options + “]”);

// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.

// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法

try {

Method execStartActivity = Instrumentation.class.getDeclaredMethod

(“execStartActivity”, Context.class, IBinder.class, IBinder.class, Activity

.class, Intent.class, int.class, Bundle.class);

execStartActivity.setAccessible(true);

return (ActivityResult) execStartActivity.invoke(mBase, who, contextThread, token,

target, intent, requestCode, options);

} catch (Exception e) {

// rom修改了 需要手动适配

throw new RuntimeException(“do not support!!! pls adapt it”);

}

}

}

可以看到在 ApplicationInstrumentation 里面,我们只是打印出 startActivity 中各个方法参数的值。

运行以下测试代码

try {

HookHelper.attachContext();

} catch (Exception e) {

e.printStackTrace();

}

Intent intent = new Intent(this, TestActivityStart.class);

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

getApplicationContext().startActivity(intent);

将看到以下 log


Hook AMS


上面 hook activity 的两种方法其实都有一定缺陷,比如,第一种方法,只能 hook 住通过 Activity startActivity 的 activity。第二种方法,只能 hook 住通过 getApplicationContext().startActivity 启动的 activity。那有没有一种方法能 hook 上述两种的,其实是有的,那就是 hook AMS。下面让我们一起来看一下。

上面 hook startActivity 其实都是 hook 相应的 mInstrumentation.execStartActivity 方法,因此,我们可以从这里下手,看 mInstrumentation.execStartActivity 里面有没有一些共性的东西,可以 hook。

我们先来 mInstrumentation.execStartActivity 方法

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, Activity target,

Intent intent, int requestCode, Bundle options) {

IApplicationThread whoThread = (IApplicationThread) contextThread;

Uri referrer = target != null ? target.onProvideReferrer() : null;

if (referrer != null) {

intent.putExtra(Intent.EXTRA_REFERRER, referrer);

}

if (mActivityMonitors != null) {

synchronized (mSync) {

final int N = mActivityMonitors.size();

for (int i=0; i<N; i++) {

final ActivityMonitor am = mActivityMonitors.get(i);

ActivityResult result = null;

if (am.ignoreMatchingSpecificIntents()) {

result = am.onStartActivity(intent);

}

if (result != null) {

am.mHits++;

return result;

} else if (am.match(who, null, intent)) {

am.mHits++;

if (am.isBlocking()) {

return requestCode >= 0 ? am.getResult() : null;

}

break;

}

}

}

}

try {

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess(who);

int result = ActivityManager.getService()

.startActivity(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, target != null ? target.mEmbeddedID : null,

requestCode, 0, null, options);

checkStartActivityResult(result, intent);

} catch (RemoteException e) {

throw new RuntimeException(“Failure from system”, e);

}

return null;

}

这里我们留意 ActivityManager.getService().startActivity 这个方法

public static IActivityManager getService() {

return IActivityManagerSingleton.get();

}

private static final Singleton IActivityManagerSingleton =

new Singleton() {

@Override

protected IActivityManager create() {

final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);

final IActivityManager am = IActivityManager.Stub.asInterface(b);

return am;

}

};

可以看到 IActivityManagerSingleton 是一个单例对象,因此,我们可以 hook 它。

public static void hookAMSAfter26() throws Exception {

// 第一步:获取 IActivityManagerSingleton

Class<?> aClass = Class.forName(“android.app.ActivityManager”);

Field declaredField = aClass.getDeclaredField(“IActivityManagerSingleton”);

declaredField.setAccessible(true);

Object value = declaredField.get(null);

Class<?> singletonClz = Class.forName(“android.util.Singleton”);

Field instanceField = singletonClz.getDeclaredField(“mInstance”);

instanceField.setAccessible(true);

Object iActivityManagerObject = instanceField.get(value);

// 第二步:获取我们的代理对象,这里因为 IActivityManager 是接口,我们使用动态代理的方式

Class<?> iActivity = Class.forName(“android.app.IActivityManager”);

InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new

Class<?>[]{iActivity}, handler);

// 第三步:偷梁换柱,将我们的 proxy 替换原来的对象

instanceField.set(value, proxy);

}

public class AMSInvocationHandler implements InvocationHandler {

private static final String TAG = “AMSInvocationHandler”;

Object iamObject;

public AMSInvocationHandler(Object iamObject) {

this.iamObject = iamObject;

}

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// Log.e(TAG, method.getName());

if (“startActivity”.equals(method.getName())) {

Log.i(TAG, “ready to startActivity”);

for (Object object : args) {

Log.d(TAG, “invoke: object=” + object);

}

}

return method.invoke(iamObject, args);

}

}

执行以下测试代码

try {

HookHelper.hookAMS();

} catch (Exception e) {

e.printStackTrace();

}

startActivity(new Intent(this,TestActivityStart.class));

将会看到以下 log

I/AMSInvocationHandler: ready to startActivity

接下来我们一起来看一下 API 25 Instrumentation 的代码(自 API 26 开始 ,Instrumentation execStartActivity 方法有所改变)

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, Activity target,

Intent intent, int requestCode, Bundle options) {

IApplicationThread whoThread = (IApplicationThread) contextThread;

Uri referrer = target != null ? target.onProvideReferrer() : null;

if (referrer != null) {

intent.putExtra(Intent.EXTRA_REFERRER, referrer);

}

if (mActivityMonitors != null) {

synchronized (mSync) {

final int N = mActivityMonitors.size();

for (int i=0; i<N; i++) {

final ActivityMonitor am = mActivityMonitors.get(i);

if (am.match(who, null, intent)) {

am.mHits++;

if (am.isBlocking()) {

return requestCode >= 0 ? am.getResult() : null;

}

break;

}

}

}

}

try {

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess(who);

int result = ActivityManagerNative.getDefault()

.startActivity(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, target != null ? target.mEmbeddedID : null,

requestCode, 0, null, options);

checkStartActivityResult(result, intent);

} catch (RemoteException e) {

throw new RuntimeException(“Failure from system”, e);

}

return null;

}

可以看到这里启动 activity 是调用 ActivityManagerNative.getDefault().startActivity 启动的。

public abstract class ActivityManagerNative extends Binder implements IActivityManager

{

/**

  • Retrieve the system’s default/global activity manager.

*/

static public IActivityManager getDefault() {

return gDefault.get();

}

private static final Singleton gDefault = new Singleton() {

protected IActivityManager create() {

IBinder b = ServiceManager.getService(“activity”);

if (false) {

Log.v(“ActivityManager”, "default service binder = " + b);

}

IActivityManager am = asInterface(b);

if (false) {

Log.v(“ActivityManager”, "default service = " + am);

}

return am;

}

};

}

同理我们看到 ActivityManagerNative 的 gDefault 是一个静态变量,因此,我们可以尝试 hook gDefault.

public static void hookAmsBefore26() throws Exception {

// 第一步:获取 IActivityManagerSingleton

Class<?> forName = Class.forName(“android.app.ActivityManagerNative”);

Field defaultField = forName.getDeclaredField(“gDefault”);

defaultField.setAccessible(true);

Object defaultValue = defaultField.get(null);

Class<?> forName2 = Class.forName(“android.util.Singleton”);

Field instanceField = forName2.getDeclaredField(“mInstance”);

instanceField.setAccessible(true);

Object iActivityManagerObject = instanceField.get(defaultValue);

// 第二步:获取我们的代理对象,这里因为 IActivityManager 是接口,我们使用动态代理的方式

Class<?> iActivity = Class.forName(“android.app.IActivityManager”);

InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivity}, handler);

// 第三步:偷梁换柱,将我们的 proxy 替换原来的对象

instanceField.set(defaultValue, proxy);

}

到此,hook Activity 的三种方式已讲解完毕


启动一个没有在 AndroidManifest 声明的 Activity


我们知道,当我们启动一个没有在 AndroidManifest 中声明的 activity,会抛出 ActivityNotFoundException 异常。

Caused by: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.xj.hookdemo/com.xj.hookdemo.activityhook.TargetAppCompatActivity}; have you declared this activity in your AndroidManifest.xml?

at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2124)

at android.app.Instrumentation.execStartActivity(Instrumentation.java:1802)

at android.app.Activity.startActivityForResult(Activity.java:4514)

at android.support.v4.app.BaseFragmentActivityApi16.startActivityForResult(BaseFragmentActivityApi16.java:54)

at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:67)

at android.app.Activity.startActivityForResult(Activity.java:4472)

at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:720)

at android.app.Activity.startActivity(Activity.java:4833)

at android.app.Activity.startActivity(Activity.java:4801)

at com.xj.hookdemo.activityhook.TestStartActivityNoRegister.onB

从报错的堆栈中,我们非常定位到 Instrumentation.execStartActivity 方法

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, String resultWho,

Intent intent, int requestCode, Bundle options, UserHandle user) {

----- // 省略若干代码

try {

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess(who);

int result = ActivityManager.getService()

.startActivityAsUser(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, resultWho,

requestCode, 0, null, options, user.getIdentifier());

checkStartActivityResult(result, intent);

} catch (RemoteException e) {

throw new RuntimeException(“Failure from system”, e);

}

return null;

}

在该方法中,调用 startActivityAsUser 方法通过传入的 intent 获取 result,再通过 checkStartActivityResult 方法,判断 result 是否合法。

而我们知道我们启动的 activity 信息都储存在 intent 中,那么我们若想要 启动一个没有在 AndroidManifest 声明的 Activity,那我们只需要在 某个时机,即调用 startActivity 方法之前欺骗 AMS 我们的 activity 已经注册(即替换 intent),这样就不会抛出 ActivityNotFoundException 异常。

在前面的时候,我们已经讲解到如何 hook ams,这里我们不再具体讲述,主要步骤如下

  • 第一步, API 26 以后,hook android.app.ActivityManager.IActivityManagerSingleton, API 25 以前,hook android.app.ActivityManagerNative.gDefault

  • 第二步,获取我们的代理对象,这里因为是接口,所以我们使用动态代理的方式

  • 第三步:设置为我们的代理对象

private static void hookAMS(Context context) throws ClassNotFoundException,

NoSuchFieldException, IllegalAccessException {

// 第一步, API 26 以后,hook android.app.ActivityManager.IActivityManagerSingleton,

// API 25 以前,hook android.app.ActivityManagerNative.gDefault

Field gDefaultField = null;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

Class<?> activityManager = Class.forName(“android.app.ActivityManager”);

gDefaultField = activityManager.getDeclaredField(“IActivityManagerSingleton”);

} else {

Class<?> activityManagerNativeClass = Class.forName(“android.app.ActivityManagerNative”);

gDefaultField = activityManagerNativeClass.getDeclaredField(“gDefault”);

}

gDefaultField.setAccessible(true);

Object gDefaultObj = gDefaultField.get(null); //所有静态对象的反射可以通过传null获取。如果是实列必须传实例

Class<?> singletonClazz = Class.forName(“android.util.Singleton”);

Field amsField = singletonClazz.getDeclaredField(“mInstance”);

amsField.setAccessible(true);

Object amsObj = amsField.get(gDefaultObj);

//

String pmName = getPMName(context);

String hostClzName = getHostClzName(context, pmName);

// 第二步,获取我们的代理对象,这里因为是接口,所以我们使用动态代理的方式

amsObj = Proxy.newProxyInstance(context.getClass().getClassLoader(), amsObj.getClass()

.getInterfaces(), new AMSHookInvocationHandler(amsObj, pmName, hostClzName));

// 第三步:设置为我们的代理对象

amsField.set(gDefaultObj, amsObj);

}

接着,我们在动态代理对象中,当调用 startActivity 方法的时候,我们把 intent 信息替换,校验的时候就可以绕过系统对 activity 的校验,这样就不会跑出 ActivityNotFoundException 异常。

public class AMSHookInvocationHandler implements InvocationHandler {

public static final String ORIGINALLY_INTENT = “originallyIntent”;

private Object mAmsObj;

private String mPackageName;

private String cls;

public AMSHookInvocationHandler(Object amsObj, String packageName, String cls) {

this.mAmsObj = amsObj;

this.mPackageName = packageName;

this.cls = cls;

}

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// 对 startActivity进行Hook

if (method.getName().equals(“startActivity”)) {

int index = 0;

// 找到我们启动时的intent

for (int i = 0; i < args.length; i++) {

if (args[i] instanceof Intent) {

index = i;

break;

}

}

// 取出在真实的Intent

Intent originallyIntent = (Intent) args[index];

Log.i(“AMSHookUtil”, “AMSHookInvocationHandler:” + originallyIntent.getComponent()

.getClassName());

// 自己伪造一个配置文件已注册过的Activity Intent

Intent proxyIntent = new Intent();

// 因为我们调用的Activity没有注册,所以这里我们先偷偷换成已注册。使用一个假的Intent

ComponentName componentName = new ComponentName(mPackageName, cls);

proxyIntent.setComponent(componentName);

// 在这里把未注册的Intent先存起来 一会儿我们需要在Handle里取出来用

proxyIntent.putExtra(ORIGINALLY_INTENT, originallyIntent);

args[index] = proxyIntent;

}

return method.invoke(mAmsObj, args);

}

}

但是,如果仅仅这样做,会存在一个问题,因为 intent 信息在校验的时候被我们替换了,但是我们并没有将其还原,这样,启动的 activity 就不是我们想要的 activity。

那么,我们要在哪个实际将 intent 信息还原呢?

我们回过头再来看一下 Activity 的 startActivityForResult 方法

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,

@Nullable Bundle options) {

if (mParent == null) {

options = transferSpringboardActivityOptions(options);

Instrumentation.ActivityResult ar =

mInstrumentation.execStartActivity(

this, mMainThread.getApplicationThread(), mToken, this,

intent, requestCode, options);

if (ar != null) {

mMainThread.sendActivityResult(

mToken, mEmbeddedID, requestCode, ar.getResultCode(),

ar.getResultData());

}

if (requestCode >= 0) {

// If this start is requesting a result, we can avoid making

// the activity visible until the result is received. Setting

// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the

// activity hidden during this time, to avoid flickering.

// This can only be done when a result is requested because

// that guarantees we will get information back when the

// activity is finished, no matter what happens to it.

mStartedActivity = true;

}

cancelInputsAndStartExitTransition(options);

// TODO Consider clearing/flushing other event sources and events for child windows.

} else {

if (options != null) {

mParent.startActivityFromChild(this, intent, requestCode, options);

} else {

// Note we want to go through this method for compatibility with

// existing applications that may have overridden it.

mParent.startActivityFromChild(this, intent, requestCode);

}

}

}

该方法主要分为两个逻辑,当 mParent 为空的时候即不为空的时候

  • 第一种情况,mParent 不为空的时候,调用到 mInstrumentation.execStartActivity 方法之后,会调用 mMainThread.sendActivityResult 方法

  • 第二种情况,当 mParent 为空的时候,会调用 mParent.startActivityFromChild

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,

int requestCode, @Nullable Bundle options) {

options = transferSpringboardActivityOptions(options);

Instrumentation.ActivityResult ar =

mInstrumentation.execStartActivity(

this, mMainThread.getApplicationThread(), mToken, child,

intent, requestCode, options);

if (ar != null) {

mMainThread.sendActivityResult(

mToken, child.mEmbeddedID, requestCode,

ar.getResultCode(), ar.getResultData());

}

cancelInputsAndStartExitTransition(options);

}

在 startActivityFromChild 方法里面,又会调用到 mMainThread.sendActivityResult 方法。因此,我们只需看一下该方法是怎样 send ActivityResult 的。

public final class ActivityThread {


public final void sendActivityResult(

IBinder token, String id, int requestCode,

int resultCode, Intent data) {

最后,如果大伙有什么好的学习方法或建议欢迎大家在评论中积极留言哈,希望大家能够共同学习、共同努力、共同进步。

小编在这里祝小伙伴们在未来的日子里都可以 升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰!!

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值