Android开发应用笔记

Android开发笔记

一、Android基本介绍
  1. Android系统四大组件:Activity, 服务, 广播接收器,内容提供程序。Activity是与用户交互的入口点。服务是一个通用入口点,用于因各种原因使应用在后台保持运行状态,用于执行长时间运行的操作或为远程进程执行作业。借助广播接收器组件,系统能够在常规用户流之外向应用传递事件,从而允许应用响应系统范围内的广播通知,广播接收器作为 BroadcastReceiver 的子类实现,并且每条广播都作为 Intent 对象进行传递。内容提供程序管理一组共享的应用数据,您可以将这些数据存储在文件系统、SQLite 数据库、网络中或者您的应用可访问的任何其他持久化存储位置。内容提供程序也适用于读取和写入您的应用不共享的私有数据。

  2. 组件的启动方法:

    • 如要启动 Activity,您可以向 startActivity()startActivityForResult() 传递 Intent(当您想让 Activity 返回结果时),或者为其安排新任务。
    • 可以使用 JobScheduler 类来调度操作。对于早期 Android 版本,您可以通过向 startService() 传递 Intent 来启动服务(或对执行中的服务下达新指令)。您也可通过向将 bindService() 传递 Intent 来绑定到该服务。
    • 您可以通过向 sendBroadcast()sendOrderedBroadcast()sendStickyBroadcast() 等方法传递 Intent 来发起广播。
    • 您可以通过在 ContentResolver 上调用 query(),对内容提供程序执行查询。
  3. Log方法快捷键:在onCreate()方法外输入logt,可以自动生成一个以当前类名作为值的TAG常量。

  4. 对于您在 Android 项目中加入的每一项资源,SDK 构建工具均会定义唯一的整型 ID,您可以利用此 ID 来引用资源,这些资源或来自应用代码,或来自 XML 中定义的其他资源。

  5. Android整体系统框架:

     The Android stack
二、活动Activity介绍
  1. 一个Activity通常和一个UI布局(xml文件)相关联,其命名格式为activity_name.xml。

  2. 应用中的一个 Activity 会被指定为主 Activity,这是用户启动应用时出现的第一个屏幕。然后,每个 Activity 可以启动另一个 Activity,以执行不同的操作。要在应用中使用 Activity,您必须在应用的清单中注册关于 Activity 的信息,并且必须适当地管理 Activity 的生命周期。

  3. 在onCreate()方法里可以调用setContentView()方法给当前活动加载一个布局,一般会传入一个相应资源的id。

  4. Toast方法:通过makeText()创建出一个Toast对象,然后调用show()可以显示出相应对容。

    Toast.makeText(FirstActivity.this, "click Button1", Toast.LENGTH_SHORT).show();
    
  5. 销毁一个活动时可以采用finish()方法。

  6. 声明Intent过滤器:

    <activity android:name=".ExampleActivity" android:icon="@drawable/app_icon">
        <intent-filter>
            <action android:name="android.intent.action.SEND" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="text/plain" />
        </intent-filter>
    </activity>
        
    

    action 元素指定该 Activity 会发送数据,将 category 元素声明为 DEFAULT 可使 Activity 能够接收启动请求。data 元素指定此 Activity 可以发送的数据类型。

  7. 使用Intent在多个活动之间跳转:

    • 显式Intent:

      Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
      startActivity(intent);
      
    • 隐式Intent:并不明确指出想要启动哪一个活动,而是指定一系列抽象的action和category信息,交由系统去分析Intent,找出合适的活动去启动。

      修改AndroidManifest.xml文件:

      <activity android:name=".SecondActivity">
          <intent-filter>
              <action android:name="com.example.firstactivity.ACTION_START"/>
              <category android:name="android.intent.category.DEFAULT"/>
          </intent-filter>
      </activity>
      
      Intent intent = new Intent("com.example.firstactivity.ACTION_START");
      startActivity(intent);
      
  8. 向下一个活动传递数据:

    通过putExtra()方法传输数据,它接收两个参数,第一个是键,第二个是真正传递的数据。

    Intent intent = new Intent("com.example.firstactivity.ACTION_START");
    String data = "Hello Activity";
    intent.putExtra("extra_data", data);
    startActivity(intent);
    

    通过getIntent()方法获取用于启动的Intent,然后调用getStringExtra()方法,传入相应键值,获得传递数据。

    Intent intent = getIntent();
    String data = intent.getStringExtra("extra_data");
    
  9. 返回数据给上一个活动:通过startActivityForResult()方法来启动活动,并期待活动销毁时可以返回一个结果给上一个活动。调用setResult()方法来回传数据。

    Intent intent = new Intent();
    intent.putExtra("data_return", "Hello FirstActivity");
    setResult(RESULT_OK, intent);
    finish();
    
  10. 您可以使用清单的 标记来控制哪些应用可以启动某个 Activity。父 Activity 和子 Activity 必须在其清单中具有相同的权限,前者才能启动后者。如果您为父 Activity 声明了 元素,则每个子 Activity 都必须具有匹配的 元素。

  11. 活动的生命周期:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y90PGJXS-1616739654889)(https://developer.android.com/guide/components/images/activity_lifecycle.png?hl=zh-cn)]

  12. Android使用任务Task来管理活动,一个任务就是一组存放在栈里的活动集合,该栈也被称为返回栈(Back Stack)。当启动一个新活动时,则入栈,当销毁一个活动时则出栈。每个活动有4种状态:(1)运行状态(处于返回栈的栈顶);(2)暂停状态(不在栈顶但仍可见);(4)停止状态(不处于栈顶且完全不可见);(4)销毁状态(从返回栈中移除)。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OLG23Fkz-1616739654891)(https://developer.android.com/images/fundamentals/diagram_backstack.png?hl=zh-cn)]

  13. 多个任务可以同时在后台进行。但是,如果用户同时运行很多后台任务,系统可能会为了恢复内存而开始销毁后台 Activity,导致 Activity 状态丢失。

  14. Activity 和任务的默认行为总结如下:

    • 当 Activity A 启动 Activity B 时,Activity A 会停止,但系统会保留其状态(例如滚动位置和输入到表单中的文本)。如果用户在 Activity B 中按返回按钮,系统会恢复 Activity A 及其状态。
    • 当用户通过按主屏幕按钮离开任务时,当前 Activity 会停止,其任务会转到后台。系统会保留任务中每个 Activity 的状态。如果用户稍后通过点按该任务的启动器图标来恢复该任务,该任务会进入前台并恢复堆栈顶部的 Activity。
    • 如果用户按返回按钮,当前 Activity 将从堆栈中退出并销毁。堆栈中的上一个 Activity 将恢复。Activity 被销毁后,系统不会保留该 Activity 的状态。
    • Activity 可以多次实例化,甚至是从其他任务对其进行实例化。
  15. 调用Activity的onSaveInstanceState()时间:

    • 屏幕旋转重建会调用onSaveInstanceState();
    • 启动另一个activity: 当前activity在离开前会调用onSaveInstanceState();
    • 按Home键的情形和启动另一个activity一样, 当前activity在离开前会onSaveInstanceState()。
  16. 不调用Activity的onSaveInstanceState和onRestoreInstanceState()时间:

    • 用户主动finish()掉的activity不会调用onSaveInstanceState(), 包括主动按back退出的情况。
    • 新建的activity, 从onCreate()开始, 不会调用onRestoreInstanceState()。
  17. Activity A 启动 Activity B 时的操作发生顺序:

    • Activity A 的 onPause() 方法执行。
    • Activity B 的 onCreate()onStart()onResume() 方法依次执行(Activity B 现在具有用户焦点)。
    • 然后,如果 Activity A 在屏幕上不再显示,其 onStop() 方法执行。
  18. Activity的启动模式:

    • standard:系统在启动该 Activity 的任务中创建 Activity 的新实例,并将 intent 传送给该实例。在这种情况下,栈顶实例会被多次创建。

    • singleTop:如果当前任务的顶部已存在 Activity 的实例,则系统会通过调用其 onNewIntent() 方法来将 intent 转送给该实例,而不是创建 Activity 的新实例。在这种情况下,栈顶实例只会被创建一次。

    • singleTask:系统会创建新任务,并实例化新任务的根 Activity。但是,如果另外的任务中已存在该 Activity 的实例,则系统会通过调用其 onNewIntent() 方法将 intent 转送到该现有实例,而不是创建新实例,但该实例之上的所有活动会被出栈。Activity 一次只能有一个实例存在。

      img
    • singleInstance:系统不会将任何其他 Activity 启动到包含该实例的任务中。该 Activity 始终是其任务唯一的成员;由该 Activity 启动的任何 Activity 都会在其他的任务中打开。

  19. Intent标记:

    • FLAG_ACTIVITY_NEW_TASK:等同于singleTask;
    • FLAG_ACTIVITY_SINGLE_TOP:等同于singleTop;
    • FLAG_ACTIVITY_CLEAR_TOP:如果要启动的 Activity 已经在当前任务中运行,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过 onNewIntent() 将此 intent 传送给它的已恢复实例。
三、Android UI界面设计
    • TextView::展示文本内容
    • EditText:用户输入和编辑文本
    • Button(RadioButton, CheckBox, Spinner):交互式按键
    • ScrollView, RecycleView:展示滑动主题内容
    • ImageView:展示图片
    • ConstrainLayout, LineatLayout:包含其他view元素并定位
  1. 可以使用android:gravity来指定文字的对齐方式,可选值有top, bottom, left, right, center。使用android:layout_weight属性来按照一定比例方式来指定控件大小。

  2. match_parent表示让当前元素和父元素一样,wrap_content表示当前元素刚好可以包含里面内容。不要使用绝对单位(如像素)来指定布局宽度和高度。更好的方法是使用相对测量单位(如与密度无关的像素单位 dp、wrap_content 或 match_parent),因为这样有助于确保您的应用在各类尺寸的设备屏幕上正确显示。

  3. View 通常用于绘制用户可看到并与之交互的内容,例如Button或TextView。ViewGroup 则是不可见的容器,用于定义 View 和其他 ViewGroup 对象的布局结构,例如LinearLayout和ConstraintLayout。

    img
  4. 界面XML文件编写:每个布局文件都必须只包含一个根元素,并且该元素必须是视图对象或 ViewGroup 对象。定义根元素后,您可以子元素的形式添加其他布局对象或微件,从而逐步构建定义布局的视图层次结构。任何 View 对象均可拥有与之关联的整型 ID,用于在结构树中对 View 对象进行唯一标识,加号 (+) 表示这是一个新的资源名称。

    android:id="@+id/my_button"
    
  5. 线性布局(LinearLayout):一种使用单个水平行或垂直行来组织子项的布局。此布局会在窗口长度超出屏幕长度时创建滚动条。

    img
  6. 相对布局(RelativeLayout): 让您能指定子对象彼此之间的相对位置(子对象 A 在子对象 B 左侧)或子对象与父对象的相对位置(与父对象顶部对齐)。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qe0wqjjN-1616739654892)(https://developer.android.com/images/ui/relativelayout-small.png?hl=zh-cn)]

  7. 使用适配器构建布局:如果布局的内容是动态内容或未预先确定的内容,您可以使用继承 AdapterView 的布局,在运行时用视图填充布局。AdapterView 类的子类会使用 Adapter 将数据与其布局绑定。Adapter 充当数据源与 AdapterView 布局之间的中间方:Adapter 会(从数组或数据库查询等来源)检索数据,并将每个条目转换为可添加到 AdapterView 布局中的视图。

    image.png
  8. ConstraintLayout:使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局,其中所有的视图均根据同级视图与父布局之间的关系进行布局,但其灵活性要高于 RelativeLayout。定义某个视图的位置,您必须为该视图添加至少一个水平约束条件和一个垂直约束条件。每个约束条件均表示与其他视图、父布局或隐形引导线之间连接或对齐方式。

  9. 隐藏标题栏:使用ActionBar的hide()方法。

    ActionBar actionBar = getSupportActionBar();
            if(actionBar != null)
                actionBar.hide();
    
  10. RecyclerView: 用来创建动态列表,包含与您的数据对应的视图的 ViewGroupRecyclerView 会请求这些视图,并通过在 Adapter 中调用方法,将视图绑定到其数据。确定布局后,您需要实现 AdapterViewHolder。这两个类配合使用,共同定义数据的显示方式。ViewHolder 是包含列表中各列表项的布局的 View 的封装容器。Adapter 会根据需要创建 ViewHolder 对象,还会为这些视图设置数据。将视图与其数据相关联的过程称为“绑定”。Android 支持库包含三个标准布局管理器:

    • LinearLayoutManager 将各个项排列在一维列表中。将 RecyclerViewLinearLayoutManager 搭配使用可提供类似于旧版 ListView 布局的功能。
    • GridLayoutManager 将各个项排列在二维网格中,就像棋盘上的方格一样。将 RecyclerViewGridLayoutManager 搭配使用可提供类似于旧版 GridView 布局的功能。
    • StaggeredGridLayoutManager 将各个项排列在二维网格中,每一列都在前一列基础上稍微偏移,就像美国国旗中的星星一样。
    public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
    
        private String[] localDataSet;
    
        /**
         * Provide a reference to the type of views that you are using
         * (custom ViewHolder).
         */
        public static class ViewHolder extends RecyclerView.ViewHolder {
            private final TextView textView;
    
            public ViewHolder(View view) {
                super(view);
                // Define click listener for the ViewHolder's View
    
                textView = (TextView) view.findViewById(R.id.textView);
            }
    
            public TextView getTextView() {
                return textView;
            }
        }
    
        /**
         * Initialize the dataset of the Adapter.
         *
         * @param dataSet String[] containing the data to populate views to be used
         * by RecyclerView.
         */
        public CustomAdapter(String[] dataSet) {
            localDataSet = dataSet;
        }
    
        // Create new views (invoked by the layout manager)
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
            // Create a new view, which defines the UI of the list item
            View view = LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.text_row_item, viewGroup, false);
    
            return new ViewHolder(view);
        }
    
        // Replace the contents of a view (invoked by the layout manager)
        @Override
        public void onBindViewHolder(ViewHolder viewHolder, final int position) {
    
            // Get element from your dataset at this position and replace the
            // contents of the view with that element
            viewHolder.getTextView().setText(localDataSet[position]);
        }
    
        // Return the size of your dataset (invoked by the layout manager)
        @Override
        public int getItemCount() {
            return localDataSet.length;
        }
    }
    
四、应用数据和文件
  1. 保存应用数据的选项:

    • 应用专属存储空间:存储仅供应用使用的文件,可以存储到内部存储卷中的专属目录或外部存储空间中的其他专属目录。使用内部存储空间中的目录保存其他应用不应访问的敏感信息。从内部存储空间访问,可以使用 getFilesDir()getCacheDir() 方法。从外部存储空间访问,可以使用 getExternalFilesDir()getExternalCacheDir() 方法。
    • 共享存储:存储您的应用打算与其他应用共享的文件,包括媒体、文档和其他文件,使用MediaStore API,但其他应用需要 READ_EXTERNAL_STORAGE 权限。
    • 偏好设置:以键值对形式存储私有原始数据,使用Jetpack Preferences 库。
    • 数据库:使用 Room 持久性库将结构化数据存储在专用数据库中。

    在存储敏感数据(不可通过任何其他应用访问的数据)时,应使用内部存储空间、偏好设置或数据库。

  2. 文件存储方法:首先使用Context类中提供的openFileInput()和openFileOutput()方法,然后再利用java的写入写出流进行读写操作。

  3. 内部存储空间访问:一个目录专为应用的持久性文件而设计,而另一个目录包含应用的缓存文件。在访问持久性文件时有两种方法:

    • 使用 File API 访问和存储文件:

      File file = new File(context.getFilesDir(), filename);
      
    • 调用 [openFileOutput()](https://developer.android.com/reference/android/content/Context?hl=zh-cn#openFileOutput(java.lang.String, int)) 获取会写入 filesDir 目录中的文件的 FileOutputStream

      String filename = "myfile";
      String fileContents = "Hello world!";
      try (FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE)) {
          fos.write(fileContents.toByteArray());
      }
      

    创建缓存文件时,调用 [File.createTempFile()](https://developer.android.com/reference/java/io/File?hl=zh-cn#createTempFile(java.lang.String, java.lang.String)):

    File.createTempFile(filename, null, context.getCacheDir());
    

    移除缓存文件时,对代表该文件的 File 对象使用 delete() 方法,或者应用上下文的 deleteFile() 方法,并传入文件名:

    cacheFile.delete();
    context.deleteFile(cacheFileName);
    
  4. 外部存储空间访问:在尝试从外部存储空间读取应用专属数据或将应用专属数据写入外部存储空间之前,请验证该卷是否可访问。可以通过调用 Environment.getExternalStorageState() 查询该卷的状态。如果返回的状态为 MEDIA_MOUNTED,那么您就可以在外部存储空间中读取和写入应用专属文件。如果返回的状态为 MEDIA_MOUNTED_READ_ONLY,您只能读取这些文件。

  5. 共享存储空间保存:

    • 媒体内容:系统提供标准的公共目录来存储这些类型的文件,这样用户就可以将所有照片保存在一个公共位置,将所有音乐和音频文件保存在另一个公共位置,依此类推。您的应用可以使用此平台的 MediaStore API 访问此内容。如果需要访问则需要提前声明权限:

      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                       android:maxSdkVersion="28" />
      
    • 文档和其他文件:系统有一个特殊目录,用于包含其他文件类型,例如 PDF 文档和采用 EPUB 格式的图书。您的应用可以使用此平台的存储访问框架访问这些文件。

    • 访问共享数据集:当您的应用需要访问大型共享数据集时,可以先查找是否有这类缓存的数据集(称为共享数据 blob),然后再决定是否下载新副本。应用可以通过 BlobStoreManager 中的 API 访问此共享数据集功能。

  6. 保存键值对数据:可以使用SharedPreferences API,SharedPreferences 对象指向包含键值对的文件,并提供读写这些键值对的简单方法。getSharedPreferences() - 需要多个由名称(使用第一个参数指定)标识的共享偏好设置文件。getPreferences() - 只需使用 Activity 的一个共享首选项。

  7. 写入共享偏好设置:SharedPreferences 调用 edit() 以创建一个 SharedPreferences.Editorapply() 会立即更改内存中的 SharedPreferences 对象,但会将更新异步写入磁盘。或者,您也可以使用 commit() 将数据同步写入磁盘。但是,由于 commit() 是同步的,您应避免从主线程调用它,因为它可能会暂停您的界面呈现。

        SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putInt(getString(R.string.saved_high_score_key), newHighScore);
        editor.commit();
    
  8. 从共享偏好设置读取:调用 getInt()getString() 等方法,为您想要的值提供键;如果键不存在,则可以选择返回默认值。

        SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
        int defaultValue = getResources().getInteger(R.integer.saved_high_score_default_key);
        int highScore = sharedPref.getInt(getString(R.string.saved_high_score_key), defaultValue);
        
    
  9. 使用Room将数据保存到本地数据库:在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

    Room包含3个组件:(1)数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点;(2)Entity:表示数据库中的表;(3)DAO:包含用于访问数据库的方法。

    应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。 最后,应用使用实体来获取和设置与数据库中的表列相对应的值。

    img
  10. 在build.gradle文件中添加Room依赖性:

    dependencies {
      def room_version = "2.2.6"
    
      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version"
    
      // optional - RxJava support for Room
      implementation "androidx.room:room-rxjava2:$room_version"
    
      // optional - Guava support for Room, including Optional and ListenableFuture
      implementation "androidx.room:room-guava:$room_version"
    
      // optional - Test helpers
      testImplementation "androidx.room:room-testing:$room_version"
    }
    
  11. Room实体定义数据:对于每个实体,系统会在关联的 Database 对象中创建一个表,以存储这些项。每个实体必须将至少一个字段定义为主键,需要为该字段添加 @PrimaryKey 注释。如果您想让 Room 为实体分配自动 ID,则可以设置 @PrimaryKeyautoGenerate 属性。

    Room 将类名称用作数据库表名称,如果希望表具有不同的名称,可以设置 @Entity 注释的 tableName 属性。Room 将字段名称用作数据库中的列名称,如果希望列具有不同的名称,可以将 @ColumnInfo 注释添加到字段。

    如需为实体添加索引,在 @Entity 注释中添加 indices 属性,列出要在索引或复合索引中包含的列的名称。

    @Entity(indices = {@Index("name"),
                       @Index(value = {"last_name", "address"})})
    public class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
        public String address;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }
    
  12. 使用DAO访问数据:DAO 既可以是接口,也可以是抽象类。如果是抽象类,则该 DAO 可以选择有一个以 RoomDatabase 为唯一参数的构造函数。插入使用@Insert进行注释,更新使用@update,删除使用@Delete,查询信息使用@Query,允许对数据库执行读写操作,将参数传递给查询时变量名前加上:。

  13. 定义对象之间关系:创建嵌套对象时可以使用 @Embedded 注释表示要分解为表格中的子字段的对象。定义一对一关系时,将 @Relation 注释添加到子实体的实例,同时将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为引用父实体主键的子实体列的名称。

    public class Address {
        public String street;
        public String state;
        public String city;
    
        @ColumnInfo(name = "post_code") public int postCode;
    }
    
    @Entity
    public class User {
        @PrimaryKey public int id;
    
        public String firstName;
    
        @Embedded public Address address;
    }   
    
五、Android应用架构设计
  1. 常见架构原则:(1)分离关注点,基于界面的类应仅包含处理界面和操作系统交互的逻辑。(2)通过模型驱动界面:模型是负责处理应用数据的组件,它们独立于应用中的 View 对象和应用组件,因此不受应用的生命周期以及相关的关注点的影响。

  2. 应用模块交互设计:每个组件仅依赖于其下一级的组件。例如,Activity 和 Fragment 仅依赖于视图模型。存储区是唯一依赖于其他多个类的类,例如存储区依赖于持久性数据模型和远程后端数据源。

    (1)构建界面:ViewModel 对象为特定的界面组件(如 Fragment 或 Activity)提供数据,并包含数据处理业务逻辑,以与模型进行通信。

    (2)获取数据:可以在 Android 应用中采用依赖项注入模式并使用 Hilt 库。Hilt 通过遍历依赖项树自动构造对象,为依赖项提供编译时保证,并为 Android 框架类创建依赖项容器。

    (3)连接ViewModel与存储区。

    (4)缓存数据:向 UserRepository 添加了一个新的数据源,用于将 User 对象缓存在内存中。

    (5)保留数据:使用Room持久性模型。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值