Activity之间的通信方式

Activity之间的通信方式

假定一个场景,Activity1需要发送数据给到Activity2,并获取Activity2的返回的数据,例如:
在这里插入图片描述
输入信息,按下右下角按钮时,需要跳转到Activity2,同时携带Activity1中的数据。Activity2获取信息并弹出:
在这里插入图片描述
Activity2输入信息,按左下角返回Activity1,将数据返回:
在这里插入图片描述
实现该场景,可通过如下几种方式:

1、通过Intent跳转时携带Bundle

Intent是组件通信的载体,通过startActivity(Intent)方法跳转Activity,将数据由Intent中的Bundle携带。Bundle可以携带各种基本类型、包装类型数据,也可以携带序列化后的对象。
Serializable和Parcelable是常见的两种序列化方式,Bundle都可携带(Parcelable需要API Level>=33),eg:
跳转时携带数据:

Intent intent = new Intent(this, Activirty2.class);
MyUser myUser = new MyUser(account.getText().toString(), password.getText().toString(), 0, null);

// 1、创建Bundle
// Bundle bundle = new Bundle();
// bundle.putParcelable("user", myUser);
// intent.putExtras(bundle);

// 2、Intent的putExtra,本质上也是放到Bundle中,该方法会自动判断Intent是否已绑定Bundle,有则放入,没有则创建Bundle后放入
intent.putExtra("user", myUser);
startActivity(intent);

获取Bundle携带的数据:

private void getMessage() {
    // getIntent()方法取出Intent
    Intent intent = getIntent();
    if(intent != null){
        MyUser myUser = intent.getParcelableExtra("user", MyUser.class);
        if(myUser!=null){
            Log.e(TAG, "onCreate: "+ myUser.toString());
            Toast.makeText(this, myUser.toString(), Toast.LENGTH_LONG).show();
        }

    }
}

上述方法在onCreate生命周期函数中调用,即可获取数据。

也可以通过startActivityForResult(Intent, int):第二个参数为requestCode,用于标识请求源,重写onActivityResult方法可以获取新启动的Activity的返回结果:
已过时

//启动Activity
startActivityForResult(intent,1)
//重写onActivityResult处理返回结果
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data)
    if(requestCode == 1 && resultCode == Activity.RESULT_OK){
        // 处理第二个页面带回的数据
    }
}

2、通过类的静态变量

通过类的静态变量,可以互相访问,从而达到相互通信。
两个Activity都创建用于相互访问的静态类变量,eg:
Activity1中:

//定义类的静态变量
public static MyUser myUser;

//跳转前更新好静态变量
myUser = new MyUser(account.getText().toString(), password.getText().toString(), 0, null);
Intent intent = new Intent(this, Activirty2.class);
startActivity(intent);

//获取数据时,访问Activirty2的静态类变量
if(Activirty2.myUser!=null){
    Log.e(TAG, "onCreate: "+ Activirty2.myUser.toString());
    Toast.makeText(this, Activirty2.myUser.toString(), Toast.LENGTH_LONG).show();
}

Activity2同理

3、通过Application

定义一个用于通信的类,eg:MyApplication,继承Application:

public class MyApplication extends Application {

    private MyUser myUser;

    public MyUser getMyUser() {
        return myUser;
    }

    public void setMyUser(MyUser myUser) {
        this.myUser = myUser;
    }
}

需要在AndroidManifest.xml中声明

<application
        android:name=".MyApplication"
        ...>
</application>

通过

MyApplication application = (MyApplication) getApplicationContext();

即可操作全局变量

4、借助外部存储

  • SharedPreference:六种简单数据的存储:string,set,int,long,float,boolean
  • SQLite
  • 文件读写

5、Activity Results API

Google推荐的Activity、Fragment获取数据的方式
两个重要的组件:

  • ActivityResultContract:抽象基类,需要继承它来实现自己的协议,从而定义如何传递、处理数据,需要定义输入类和输出类,无需输入可用Void
  • ActivityResultLauncher:
    • 调用registerForActivityResult(ActivityResultContract, ActivityResultCallback)方法注册ActivityResultLauncher,第一个参数为协议,可以使用预定义的,第二个参数定义跳转回来的回调函数
    • 调用launch()方法来启动页面跳转

5.1、使用Activity Results API需要加入依赖:

implementation 'androidx.activity:activity:1.4.0'
implementation 'androidx.fragment:fragment:1.4.1'

5.2、调用预定义的Contract

预定义Contract释义
StartActivityForResult()通用的Contract,不做任何转换,Intent作为输入,ActivityResult作为输出,这也是最常用的一个协定。
RequestMultiplePermissions()用于请求一组权限
RequestPermission()用于请求单个权限
TakePicturePreview()调用MediaStore.ACTION_IMAGE_CAPTURE拍照,返回值为Bitmap图片
TakePicture()调用MediaStore.ACTION_IMAGE_CAPTURE拍照,并将图片保存到给定的Uri地址,返回true表示保存成功。
TakeVideo()调用MediaStore.ACTION_VIDEO_CAPTURE 拍摄视频,保存到给定的Uri地址,返回一张缩略图。
PickContact()从通讯录APP获取联系人
CreateDocument()提示用户选择一个文档,返回一个(file:/http:/content:)开头的Uri。
OpenDocumentTree()提示用户选择一个目录,并返回用户选择的作为一个Uri返回,应用程序可以完全管理返回目录中的文档。
OpenMultipleDocuments()提示用户选择文档(可以选择多个),分别返回它们的Uri,以List的形式。
OpenDocument()提示用户选择文档,返回它的Uri
GetContent()提示用户选择一条内容,返回一个通过ContentResolver#openInputStream(Uri)访问原生数据的Uri地址(content://形式) 。默认情况下,它增加了 Intent#CATEGORY_OPENABLE, 返回可以表示流的内容。

使用最多的就是StartActivityForResult和RequestMultiplePermissions了。

(1)StartActivityForResult完成页面跳转

Activity1:

// 声明ActivityResultLauncher
private ActivityResultLauncher<Intent> activityResultLauncher;

//只能在onCreate、onAttach方法中注册
activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
  	@Override
    public void onActivityResult(ActivityResult result) {
        MyUser user = result.getData().getParcelableExtra("user", MyUser.class);
        if(user!=null){
            Log.e(TAG, "onCreate: "+ user.toString());
            Toast.makeText(MainActivity.this, user.toString(), Toast.LENGTH_LONG).show();
        }
    }
});

//点击按钮跳转
Intent intent = new Intent(this, Activirty2.class);
MyUser myUser = new MyUser(account.getText().toString(), password.getText().toString(), 0, null);
intent.putExtra("user", myUser);
activityResultLauncher.launch(intent);

Activity2:

// onCreate中获取Intent
Intent intent = getIntent();
if(intent != null){
     myUser = intent.getParcelableExtra("user", MyUser.class);
     Log.e(TAG, "onCreate: "+ myUser.toString());
     Toast.makeText(this, myUser.toString(), Toast.LENGTH_LONG).show();
 }
 
// 点击按钮时返回Activity1
Intent intent = new Intent();
if(myUser != null){
    myUser.setAge(Integer.parseInt(age.getText().toString()));
    myUser.setPhone(phone.getText().toString());
}
intent.putExtra("user", myUser);
setResult(RESULT_OK, intent);
finish();
(2)申请单个或多个权限

首先AndroidManifest.xml文件中需要加入权限声明:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
private ActivityResultLauncher<String> permissionLauncher;

private ActivityResultLauncher<String[]> multiplePermissionsLauncher;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    permissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>() {
        @Override
        public void onActivityResult(Boolean result) {
            Log.d(TAG, "onActivityResult: "+(result? "权限申请成功" : "权限申请失败"));
            if(result){
                Toast.makeText(MainActivity.this, "权限申请成功", Toast.LENGTH_LONG).show();
                return;
            }
            boolean b = shouldShowRequestPermissionRationale(Manifest.permission.CAMERA);
            if(b){
                // 被拒绝
                Toast.makeText(MainActivity.this, "被用户拒绝", Toast.LENGTH_LONG).show();
            }else{
                Toast.makeText(MainActivity.this, "被用户拒绝,申请时或之前选择不再提示,智能跳转设置页手动赋予权限", Toast.LENGTH_LONG).show();
            }
        }
    });
    Button btnRequestPermission = findViewById(R.id.btn_RequestPermission);
    btnRequestPermission.setOnClickListener(v->permissionLauncher.launch(Manifest.permission.CAMERA));

    multiplePermissionsLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
        @Override
        public void onActivityResult(Map<String, Boolean> result) {
            result.entrySet().forEach(entry -> {
                Log.d(TAG, entry.getKey()+": "+(entry.getValue() ? "权限申请成功" : "权限申请失败"));
            });
        }
    });
    Button btnRequestMultiplePermissions = findViewById(R.id.btn_RequestMultiplePermissions);
    btnRequestMultiplePermissions.setOnClickListener(v->
            multiplePermissionsLauncher.launch(new String[]{Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION}));
}
(3)调用相机拍照

首先需要在res/xml下新增file_path.xml文件,如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path
        name="images"
        path="." />
    <cache-path
        name="cache-path"
        path="." />
    <external-path
        name="external_storage_root"
        path="." />
    <external-cache-path
        name="external_cache_path"
        path="." />
</paths>

然后在AndroidManifest.xml注册provider

<provider
    android:authorities="${applicationId}.fileprovider"
    android:name="androidx.core.content.FileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_path" />
</provider>
private ActivityResultLauncher<Uri>  takePictureLauncher;
private ActivityResultLauncher<Void> takePicturePreviewLauncher;
private Uri imageUri;

//拍照返回缩略图
takePicturePreviewLauncher = registerForActivityResult(new ActivityResultContracts.TakePicturePreview(), new ActivityResultCallback<Bitmap>() {
    @Override
    public void onActivityResult(Bitmap result) {
        ivResult.setImageBitmap(result);
    }
});
btnCaptureSmall.setOnClickListener(v->{
    imageUri = getSaveUri(1);
    //判断是否授权:照相
    if(!isGranted(Manifest.permission.CAMERA)){
        permissionLauncher.launch(Manifest.permission.CAMERA);
    }else{
        takePicturePreviewLauncher.launch(null);
    }
});

// 拍照
takePictureLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), new ActivityResultCallback<Boolean>() {
    @Override
    public void onActivityResult(Boolean result) {
        if(result){
            ivResult.setImageURI(imageUri);
        }else{
            Toast.makeText(MainActivity.this, "出错了55555555", Toast.LENGTH_LONG).show();
        }
    }
});
btnCaptureRaw.setOnClickListener(v->{
    imageUri = getSaveUri(1);
    //判断是否授权:照相
    if(!isGranted(Manifest.permission.CAMERA)){
        permissionLauncher.launch(Manifest.permission.CAMERA);
    }else{
        takePictureLauncher.launch(imageUri);
    }
});

private Uri getSaveUri(int type)  {
  	File storageFile;
    if(Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()){
        storageFile = getExternalCacheDir();
    }else{
        storageFile = getCacheDir();
    }
    File file = null;
    try {
        if(type == 1){
            //photo
            file = File.createTempFile("tmp_iamge_file", ".png", storageFile);
            file.createNewFile(); //创建新的临时文件
//            photoFile.deleteOnExit();  //推出JVM时删除
        }else if(type == 2){
            // vedio
            file = File.createTempFile("tmp_iamge_file", ".mp4", storageFile);
            file.createNewFile(); //创建新的临时文件
        }else{
            // 待新增
        }

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    Uri res = FileProvider.getUriForFile(this, getApplicationContext().getPackageName()+".fileprovider", file);
    return res;
}

private boolean isGranted(String permission){
    return ActivityCompat.checkSelfPermission(MainActivity.this, permission) == PackageManager.PERMISSION_GRANTED;
}

(4)调用相机录像
private Uri videoUri;
private ActivityResultLauncher<Uri> takeVideoLauncher;
private boolean isPlay;

// 录视频
takeVideoLauncher = registerForActivityResult(new ActivityResultContracts.TakeVideo(), new ActivityResultCallback<Bitmap>() {
    @Override
    public void onActivityResult(Bitmap result) {
        videoView.setVideoURI(videoUri);
        videoView.setVisibility(View.VISIBLE);
    }
});
btnCaptureVideo.setOnClickListener(v->{
    videoUri = getSaveUri(2);
    //判断是否授权:照相
    if(!isGranted(Manifest.permission.CAMERA)){
        permissionLauncher.launch(Manifest.permission.CAMERA);
    }else{
        takeVideoLauncher.launch(videoUri);
    }
});

videoView.setOnClickListener(v->{
	VideoView vv = (VideoView) v;
    if(isPlay){
        vv.pause();
    }else{
        vv.start();
    }
});
(5)打开相册
private ActivityResultLauncher<String> selectPhotoLauncher;

// 选择图片
selectPhotoLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), new ActivityResultCallback<Uri>() {
    @Override
    public void onActivityResult(Uri result) {
        ivResult.setImageURI(result);
    }
});
btnSelectPhoto.setOnClickListener(v->{
    selectPhotoLauncher.launch("image/*");
});

5.3、自定义ActivityResultContract

新建一个Contract类,继承ActivityResultContract<I,O>,其中,I是输入的类型,O是输出的类型。需要实现2个方法,createIntent和parseResult,输入类型I作为createIntent的参数,输出类型O作为parseResult方法的返回值。

/**
 * 自定义ActivityResultContract
 */
public class CustomActivityResultContract extends ActivityResultContract<Integer, String> {

    //input为输入值,然后包装成Intent传递
    @NotNull
    @Override
    public Intent createIntent(@NotNull Context context, Integer input) {
        //Intent包装了跳转到SecondActivity
        Intent intent = new Intent(context, SecondActivity.class);
        intent.putExtra("in", input);
        return intent;
    }

    //返回的Intent拆解,变换成String作为返回值
    @Override
    public String parseResult(int result, @Nullable Intent intent) {
        //拿到SecondActivity返回的Intent,拆解出需要的数据并返回
        return intent.getStringExtra("out");
    }
}

6、EventBus

6.1、概述

一套适用于Android和Java的事件发布/订阅框架,用于在组件/线程间通信的场景中,将数据或事件传递给对应的订阅者
在这里插入图片描述
组件/线程间通信可以有很多种方式,如接口监听、Bundle、Handler、Executors、LocalBroadcastManager,但都存在一定的问题:

  • 接口监听:强耦合
  • Bundle:组件间通信,但是不够灵活
  • Handler/Executors:主要用于线程间通信,样板代码臃肿
  • LocalBroadcastManager:没有类型安全,从Extras中取数据容易类型转换异常

EventBus的优点:

  • 使用事件总线机制,Publisher和Subscriber松耦合
  • 提供透明线程间通信,隐藏线程切换

6.2、使用步骤

  1. 添加依赖

    // Java:
    android {
        defaultConfig {
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [ eventBusIndex : 'com.have.a.good.MyEventBusAppIndex' ]
                }
            }
        }
    }
     
    dependencies {
        def eventbus_version = '3.2.0'
        implementation "org.greenrobot:eventbus:$eventbus_version"
        annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbus_version"
    }
    
  2. 准备订阅者,@Subscribe
    在这里插入图片描述

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(MessageEvent event) {
        Toast.makeText(getActivity(), event.message, Toast.LENGTH_SHORT).show();
    }
    

threadMode 指定线程模型,一共有五种
在这里插入图片描述

  1. 注册与注销
    发布事件之前,注册订阅者;订阅者生命周期结束,注销订阅者
    在这里插入图片描述

    @Override
    public void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
    }
     
    @Override
    public void onStop() {
        EventBus.getDefault().unregister(this);
        super.onStop();
    }
    
  2. 发布事件
    在这里插入图片描述

    EventBus.getDefault().post(new MessageEvent("Hello everyone!"));
    EventBus.getDefault().postSticky(new MessageEvent("Hello everyone!"));
    

    粘性事件的特点:
    在这里插入图片描述

    1、订阅
    @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
    public void onEvent(MessageEvent event) {   
        textField.setText(event.message);
    }
    
    2、发布
    EventBus.getDefault().postSticky(new MessageEvent("Hello everyone!"));
    
    3、获取粘性事件
    MessageEvent stickyEvent = EventBus.getDefault().getStickyEvent(MessageEvent.class);
    if(stickyEvent != null) {
        4、移除粘性事件
        EventBus.getDefault().removeStickyEvent(stickyEvent);
        // do something.
    }
    5、移除粘性事件
    MessageEvent stickyEvent = EventBus.getDefault().removeStickyEvent(MessageEvent.class);
    if(stickyEvent != null) {
        // do something.
    }
    

实现上文假定的场景,同样可以使用EventBus,但是应该注意的是,Activity1发布事件时,Activity2还未注册订阅者,因此需要发布粘性事件(存储在ConcurrentHashMap中),不再使用的粘性事件应该及时删除,避免内存泄漏。
Activity1、Activity2同时都是订阅者,因此应该在onCreate注册,onDestory注销

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    EventBus.getDefault().register(this);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(this);
}

定义事件,messageSource标识发布源

public class MessageEvent {
    private MyUser user;

    private String messageSource;

    public MessageEvent(MyUser user, String messageSource) {
        this.user = user;
        this.messageSource = messageSource;
    }

    public MyUser getUser() {
        return user;
    }

    public void setUser(MyUser user) {
        this.user = user;
    }

    public String getMessageSource() {
        return messageSource;
    }

    public void setMessageSource(String messageSource) {
        this.messageSource = messageSource;
    }
}

Activity1、Activity2都应该准备订阅方法,区别只是判断messageSource:

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onMessageEvent(MessageEvent messageEvent){
    if(messageEvent.getMessageSource().equals("ActivityX")){
    	// Activity2应该为成员变量赋值  
    	//myUser = messageEvent.getUser();
        Log.e(TAG, "onCreate: "+ messageEvent.getUser().toString());
        Toast.makeText(MainActivity.this, messageEvent.getUser().toString(), Toast.LENGTH_LONG).show();
        // 哈希表持有粘性事件的强引用,不再使用的粘性事件应该移除
        EventBus.getDefault().removeStickyEvent(this);
    }
}

Activity1:

// 按下按钮
MyUser myUser = new MyUser(account.getText().toString(), password.getText().toString(), 0, null);
EventBus.getDefault().postSticky(new MessageEvent(myUser, "Activity1"));
startActivity(new Intent(this, Activirty2.class));

Activity2:

// 按下按钮
if(myUser != null){
    myUser.setAge(Integer.parseInt(age.getText().toString()));
    myUser.setPhone(phone.getText().toString());
    EventBus.getDefault().postSticky(new MessageEvent(myUser, "Activity2"));
}
finish();

6.3 、编译时索引

在这里插入图片描述
待学习补充

6.4、构建者模式

可以使用EventBusBuilder构建EventBus实例,也可以EventBus.getDefault()获得默认的 EventBus 实例

  1. 异常处理配置

    配置项描述默认值
    logSubscriberExceptions订阅函数执行有异常时,打印异常信息true
    sendSubscriberExceptionEvent订阅函数执行有异常时,发布SubscriberExceptionEvent事件true
    throwSubscriberException订阅函数执行有异常时,抛出SubscriberExceptionfalse
    logNoSubscriberMessages事件无匹配订阅函数时,打印信息true
    sendNoSubscriberEvent事件无匹配订阅函数时,发布NoSubscriberEventtrue
  2. 索引配置

    配置项描述默认值
    ignoreGeneratedIndex忽略订阅者索引false
    addIndex(SubscriberInfoIndex index)添加订阅者索引
  3. 事件订阅配置

    配置项描述默认值
    eventInheritance是否触发父类事件订阅函数true
    executorService(ExecutorService executorService)线程池Executors#newCachedThreadPool()
    strictMethodVerification是否严格验证订阅函数签名数true
    skipMethodVerificationFor(Class<?> clazz)跳过方法签名验证

6.5、反混淆规则

待学习补充

本文仅个人总结,EventBus图片摘自https://www.jianshu.com/p/df4f4467e5f1

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
西 安 邮 电 大 学 (计算机学院) 课内实验报告 实验名称: 界面设计:基本组件 专 业: 网络工程 班 级: 姓 名: 学 号: 指导教师: 日 期: 2017年4月20日 一.实验目的 1. 掌握常用组件在布局文件中的设置 2. 掌握在java程序中获取组件值 3. 掌握对组件值得验证 4. 掌握基本常用的监听器,和事件处理 5. 掌握将组件值提交到下一个Activity活动的方法 二.实验环境 JDK的版本: "1.8.0_40" IDE: eclipse 4.6.1 模拟器: 夜神模拟器 三.实验内容 1. 补充完成下例空缺处,完成注册界面、部门列表框、 单击确定检查 提交成功、接受界面 四.实验过程及分析 1. 设计UI界面 1. 编写布局代码,如下 <LinearLayout xmlns:android = "http://schemas.android.com/apk/res/android" android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical"> <!-- 主布局中添加文本框和输入框 --> <TextView android:text = "@string/user" android:layout_width="match_parent" android:layout_height="wrap_content"/> <EditText android:id="@+id/name" android:layout_width="200sp" android:layout_height="wrap_content" android:hint="@string/inputuser"/> <!-- 在主布局添加文本框和密码框 --> <TextView android:text = "@string/password" android:layout_width="match_parent" android:layout_height="wrap_content"/> <EditText android:id="@+id/password" android:layout_width="200sp" android:layout_height="wrap_content" android:inputType="textPassword"/> <!-- 在主布局添加性别文本和复选框 --> <TextView android:text="@string/sex" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <RadioGroup android:id="@+id/sex" android:orientation = "horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"> <!-- 设置默认选择的是女 --> <RadioButton android:id="@+id/man" android:text="@string/man" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <RadioButton android:id="@+id/woman" android:checked="true" android:text="@string/woman" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RadioGroup> <!-- 在主布局添加联系电话文本框和输入框 --> <TextView android:text="@string/phone" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <EditText android:id="@+id/phone" android:inputType="text"phone" android:layout_width="200sp" android:layout_height="wrap_content"/> <!-- 在主布局中添加部门文本框和列表框 --> <TextView an

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值