第一行代码Android 阅读笔记 第八章(仅自用)

ContentProvider

上一章学习了数据持久化技术,其中包含有文件存储,SharePreferences和数据库存储,但该技术所保存的数据只能让当前程序访问。
而数据访问模式中的MODE_WORLD_READBLE和MODE_WORLD_WRITEABLE在android4.2就已经被废弃,android更推荐使用ContentProvider技术实现跨程序数据共享。

跨程序共享实例:第三方程序访问通讯录、短信、媒体库数据。

ContentProvider在不同应用程序之间实现了数据共享的功能,允许一个程序访问另一个程序中的数据,还能保证被访问数据的安全性,是android实现跨程序共享数据的标准方式。

ContentProvider不同于文件存储和SP,后者只有全局可读写操作模式,而CP则可选择只对哪部分数据进行共享,保证隐私数据不被泄露。

学习ContentProvider前需要先学习android运行时权限。

运行时权限

android的权限机制很早就有了,但之前起的作用有限,在6.0之后引入了运行时权限

android权限机制详解

前面我们已经使用过权限声明

这是之前监听开机广播时声明的权限,不然就会崩溃

加入权限声明后,在低于6.0的系统上安装程序时会有提醒
在这里插入图片描述
现在的应用申请的权限十分多,为避免拒绝个别权限而导致无法安装,于是就在android6.0引入运行运行时权限功能,即在软件使用到某项功能时再申请对应需要的权限,而不需要一次性全部授权。
而不是所有权限都需要在运行时申请,不停授权也是十分繁琐,android权限分为:普通权限和危险权限、特殊权限。

普通权限是指不会直接威胁到用户的安全和隐私的权限,系统自动授权,无需用户手动操作,如Broadcast中申请的监听开机等。

危险权限是指可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息,地理位置等,对于这类权限,必须由用户手动授权才可以。

在这里插入图片描述
当使用到表中的权限时,需要进行运行时权限处理,否则只需在AndroidManifest.xml添加权限声明。

tips:表格中每个危险权限都属于一个权限组,运行时权限处理使用的是权限名,原则上,用户一旦同意了某个权限申请时,同组其他权限也会被系统自动授权,但不能通过此规则实现逻辑,因为权限分组是可能被调整的。

//此处为常规写法,触发打电话,并在AndroidManifeat中声明权限
<uses-permission android:name="android.permission.CALL_PHONE" />

makeCall.setOnClickListener {
  try {
    val intent = Intent(Intent.ACTION_CALL)
    intent.data = Uri.parse("tel:10086")
    startActivity(intent)
  } catch (e: SecurityException) {
    e.printStackTrace()
  }
}

执行完后就会报错,显示拒绝权限(Permission Denial),因为这次调佣的权限是拨打电话权限,并非仅是打开电话页面

需要使用运行时权限申请

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
   makeCall.setOnClickListener {
   //1、检查用户是否已经授予权限了,
   //等于则说明已授权,直接通过call方法拨号
   //不等于则需要申请授权
    if (ContextCompat.checkSelfPermission(this,
      Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { 
      //接受3个参数,参数1为activity实例,参数2是String数组,放入申请的权限名,参数3为请求码,保证唯一即可。
             ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CALL_PHONE), 1)
 } 
    else {
      call()
   }
  }
 }
  override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
       //请求码匹配
       1 -> {//检查查询结果
             if (grantResults.isNotEmpty() &&grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                   call()
                }
            else {
                   Toast.makeText(this, "You denied the permission",Toast.LENGTH_SHORT).show()
                 }
              }
           }
 }
 private fun call() {
  try {
       val intent = Intent(Intent.ACTION_CALL)
       intent.data = Uri.parse("tel:10086")
       startActivity(intent)
      } catch (e: SecurityException) {
        e.printStackTrace()
      }
 }

可通过设置自行取消权限授予

使用ContentProvider访问其他程序中的数据

用法一般有二:使用现有的ContentProvider读取和操作相应程序中的数据或创建自己的ContentProvider,给程序的数据提供外部访问接口。

先从使用现有的ContentProvider开始
要想访问CP中共享的数据,就一定要借助ContentResolver类,可通过Context中的getContentResolever()方法获取该类实例。
ContentResolver类提供了一系列的方法用于对数据进行增删改查,分别使用insert()、update()、delete()、query()方法,有点类似SQLiteDataBase的操作函数。
区别在于,CR的增删改查不接受表名参数,而是接受URI参数,该参数称为内容URI。
内容URI为CP中的数据建立了唯一标识符,由两部分组成:authority和path。
authority是对不同的应用做区分的,为避免冲突,采用包名进行命名。
path是用于对同一程序中不同的表做区分,常添加到authority后面。
内容URI标准格式为

content://com.example.app.provider/table1
content://com.example.app.provider/table2

解析URI字符串为URI对象

val uri = Uri.parse("content://com.example.app.provider/table1")

ContentResolver的查询方法

val cursor = contentResolver.query(
 uri,
 projection,
 selection,
 selectionArgs,
 sortOrder)

在这里插入图片描述
查询返回的仍然是个Cursor对象,需要通过移动游标的位置遍历Cursor的所有行,然后取出每一行中相应列的数据。

while(cursor.moveToNext()){
    val column1 = cursor.getString(cursor.getColumnIndex("column1"))
    val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()

val values = contentValuesOf("column1" to "text","column2" to 1)
contentResolver.insert(uri,values)

val values = contentValuesOf("column1" to "")
contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))

contentResolver.delete(uri, "column2 = ?", arrayOf("1"))

获取通讯录联系人实例

//结合了前面对list adapter的使用和危险权限的申请
private val contactsList = ArrayList<String>()
private lateinit var adapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        adapter =ArrayAdapter(this,android.R.layout.simple_expandable_list_item_1,contactsList)
        contact_list.adapter = adapter
        button_s1.setOnClickListener{
            //常规思路,先查看权限是否已经被申请,若已经被申请则执行方法,未申请则申请权限
            if(ContextCompat.checkSelfPermission(this,READ_CONTACTS)!=PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, arrayOf(READ_CONTACTS),1)
            }
            else{
                readContacts()
            }
        }
    }
    //要点1:重写申请权限方法,通过请求码区分申请的权限
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when(requestCode){
            1->{
                if (grantResults.isNotEmpty()&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
                    readContacts()
                }
                else{
                    Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
    //申请到权限后的执行方法,此处的URI使用了已经封装好的常量
     private fun readContacts(){
        contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null)?.apply {
            while (moveToNext()){
            //逐行读取数据直到浮标指向为空
                val displayname = getString(getColumnIndex(
                    ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                val num = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                contactsList.add("$displayname\n$num")
            }
            adapter.notifyDataSetChanged()
            close()
        }
    }
    //要点3:所以权限都需要在androidManifest中声明
    <uses-permission android:name="android.permission.READ_CONTACTS"/>

效果如图
在这里插入图片描述

创建自己的ContentProvider

创建contentprovider的子类,并重写以下方法

  1. onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和
    升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败
  2. query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection
    参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,
    sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
  3. insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添
    加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。
  4. update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数
    据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,
    受影响的行数将作为返回值返回。
  5. delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,
    selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
  6. getType()。根据传入的内容URI返回相应的MIME类型。

我们可以通过解析URI来得知调用方期望访问的数据位置
前面提到了标准的URI写法
期望得到content://com.example.app这个应用上的table1表中的数据

content://com.example.app.provider/table1

期望得到content://com.example.app这个应用上的table1表中id为1的数据

content://com.example.app.provider/table1/1

内容URI格式主要有以上两种,可使用通配符分别匹配这两种格式的内容URI
*表示匹配任意长度的任意字符
#表示匹配任意长度的数字
匹配任意表的内容URI

content://com.example.app.provider/*

匹配table1表中任意行数据的内容URI可写成

content://com.example.app.provider/table1/#

//通过UriMatcher类匹配内容URI,
//UM提供了addURI方法,接收authority、path、自定义代码三个参数,返回URI对象。
class MyProvider : ContentProvider() {
//这四个变量等效于请求码
 private val table1Dir = 0
 private val table1Item = 1
 private val table2Dir = 2
 private val table2Item = 3
 private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
 init {
 uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)
 uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)
 uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)
 uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)
 }
 ...
 override fun query(uri: Uri, projection: Array<String>?, selection: String?,
 selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
 //通过match方法对传入的uri对象进行匹配,匹配成功后会返回自定义代码,再通过判断调用查询的数据
 when (uriMatcher.match(uri)) {
 table1Dir -> {
 // 查询table1表中的所有数据
 }
 table1Item -> {
 // 查询table1表中的单条数据
 }
 table2Dir -> {
 // 查询table2表中的所有数据
 }
 table2Item -> {
 // 查询table2表中的单条数据
 }
 }
 ...
 }
 ...
}

getType()用于获取Uri对象的对应的MIME类型,由3部分组成

  • 以vnd开头
  • 若内容URI以路径结尾,则后接 android.cursor.dir/;
  • 若内容URI以id结尾,则后接 android.cursor.item/。
  • 最后接上 vnd..

对于 content://com.example.app.provider/table1
MIME类型写成 vnd.android.cursor.dir/vnd.com.example.app.provider.table1
对于 content://com.example.app.provider/table1/1
MIME类型写成 vnd.android.cursor.item/vnd.com.example.app.provider.table1

因此getType可以这么写

class MyProvider : ContentProvider() {
 ...
 override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
   table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
   table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
   table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
   table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
   else -> null
 }
}

至此,一个完整的ContentProvider被创建完成了,现在任何一个程序都可以使用ContentResolver访问我们程序中的数据,而ContentProvider的机制保证了隐私数据不会被泄露出去。因为CRUD操作必须要匹配到相应的内容URI格式才能进行,而我们也不会将隐私数据的URI添加到UriMatcher中,因此隐私数据无法被外部程序访问。

实现跨程序开发

class DatabaseProvider : ContentProvider() {
//跟之前一样,定义4个请求码
 private val bookDir = 0
 private val bookItem = 1
 private val categoryDir = 2
 private val categoryItem = 3
 private val authority = "com.example.databasetest.provider"
 private var dbHelper: MyDatabaseHelper? = null
 //by lazy懒加载,当uriMatcher被调用时才会执行,最后一行返回给uriMatcher
 private val uriMatcher by lazy {
 val matcher = UriMatcher(UriMatcher.NO_MATCH)
 matcher.addURI(authority, "book", bookDir)
 matcher.addURI(authority, "book/#", bookItem)
 matcher.addURI(authority, "category", categoryDir)
 matcher.addURI(authority, "category/#", categoryItem)
 matcher
 }
 //当context不为空则执行{}内的方法,返回true,否则返回false
override fun onCreate() = context?.let {
 dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
 true
 } ?: false

 override fun query(uri: Uri, projection: Array<String>?, selection: String?,
 selectionArgs: Array<String>?, sortOrder: String?) = dbHelper?.let {
 // 查询数据,先获取数据库实例,并通过匹配URI,得知期望访问哪些数据
 val db = it.readableDatabase
 val cursor = when (uriMatcher.match(uri)) {
 bookDir -> db.query("Book", projection, selection, selectionArgs,
 null, null, sortOrder)
 bookItem -> {
 //访问单条数据时,调用了getPathSegments,它将内容URI权限之后的部分以/符号进行分割,分割后的结果放入一个字符串列表中,第一个位置存放的就是id
 val bookId = uri.pathSegments[1]
 db.query("Book", projection, "id = ?", arrayOf(bookId), null, null,
 sortOrder)
 }
 categoryDir -> db.query("Category", projection, selection, selectionArgs,
 null, null, sortOrder)
 categoryItem -> {
 val categoryId = uri.pathSegments[1]
 db.query("Category", projection, "id = ?", arrayOf(categoryId),
 null, null, sortOrder)
 }
 else -> null
 }
 cursor
 }
 override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
 // 添加数据,获取数据库实例,并根据内容URI匹配,得到要插入数据的表,通过db的insert插入数据
 //insert要求返回一个能表示新数据的内容URI
 val db = it.writableDatabase
 val uriReturn = when (uriMatcher.match(uri)) {
 bookDir, bookItem -> {
 val newBookId = db.insert("Book", null, values)
 Uri.parse("content://$authority/book/$newBookId")
 }
 categoryDir, categoryItem -> {
 val newCategoryId = db.insert("Category", null, values)
 Uri.parse("content://$authority/category/$newCategoryId")
 }
 else -> null
 }
 uriReturn
 }
 //update同insert差不多
 override fun update(uri: Uri, values: ContentValues?, selection: String?,
 selectionArgs: Array<String>?) = dbHelper?.let {
 // 更新数据
 val db = it.writableDatabase
 val updatedRows = when (uriMatcher.match(uri)) {
 bookDir -> db.update("Book", values, selection, selectionArgs)
 bookItem -> {
 val bookId = uri.pathSegments[1]
 db.update("Book", values, "id = ?", arrayOf(bookId))
 }
 categoryDir -> db.update("Category", values, selection, selectionArgs)
 categoryItem -> {
 val categoryId = uri.pathSegments[1]
 db.update("Category", values, "id = ?", arrayOf(categoryId))
 }
 else -> 0
 }
 updatedRows
 } ?: 0
 override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?)
 = dbHelper?.let {
 // 删除数据
www.blogss.cn
 val db = it.writableDatabase
 val deletedRows = when (uriMatcher.match(uri)) {
 bookDir -> db.delete("Book", selection, selectionArgs)
 bookItem -> {
 val bookId = uri.pathSegments[1]
 db.delete("Book", "id = ?", arrayOf(bookId))
 }
 categoryDir -> db.delete("Category", selection, selectionArgs)
 categoryItem -> {
 val categoryId = uri.pathSegments[1]
 db.delete("Category", "id = ?", arrayOf(categoryId))
 }
 else -> 0
 }
 deletedRows
 } ?: 0
 override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
 bookDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
 bookItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
 categoryDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.
 provider.category"
 categoryItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.
 provider.category"
 else -> null
 }
}

可用点击包,右键new->other->Content Provider快捷创建,自行创建则需要在AndroidManifest注册

<provider
 android:name=".DatabaseProvider"
 android:authorities="com.example.databasetest.provider"
 android:enabled="true"//是否启用
 android:exported="true">//是否可被外部程序发现
 </provider>

Activity中实现的样例代码

class MainActivity : AppCompatActivity() {
 var bookId: String? = null
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 addData.setOnClickListener {
 // 添加数据,创建内容URI
 val uri = Uri.parse("content://com.example.databasetest.provider/book")
 //创建content值
 val values = contentValuesOf("name" to "A Clash of Kings",
 "author" to "George Martin", "pages" to 1040, "price" to 22.85)
 //返回URI对象
 val newUri = contentResolver.insert(uri, values)
 //获取新增数据的id
 bookId = newUri?.pathSegments?.get(1)
 }
 queryData.setOnClickListener {
 // 查询数据,查询内容放在cursor对象
 val uri = Uri.parse("content://com.example.databasetest.provider/book")
www.blogss.cn
 contentResolver.query(uri, null, null, null, null)?.apply {
 while (moveToNext()) {
 val name = getString(getColumnIndex("name"))
 val author = getString(getColumnIndex("author"))
 val pages = getInt(getColumnIndex("pages"))
 val price = getDouble(getColumnIndex("price"))
 Log.d("MainActivity", "book name is $name")
 Log.d("MainActivity", "book author is $author")
 Log.d("MainActivity", "book pages is $pages")
 Log.d("MainActivity", "book price is $price")
 }
 close()
 }
 }
 updateData.setOnClickListener {
 // 更新数据
 bookId?.let {
 val uri = Uri.parse("content://com.example.databasetest.provider/
 book/$it")//此处将前面新增数据的id加入内容uri中,表示修改对应的数据
 val values = contentValuesOf("name" to "A Storm of Swords",
 "pages" to 1216, "price" to 24.05)
 contentResolver.update(uri, values, null, null)
 }
 }
 deleteData.setOnClickListener {
 // 删除数据
 bookId?.let {
 val uri = Uri.parse("content://com.example.databasetest.provider/
 book/$it")//与更新数据同理,只删除对应id的数据
 contentResolver.delete(uri, null, null)
 }
 }
 }
}

泛型

kotlin中的泛型和java的有所不同
泛型是指我们在不指定具体类型的情况下进行编程,使得代码具有更好的扩展性
如List,是个能存放数据的列表,但List没有限制我们只能存放哪种类型的数据,使用了泛型,因此我们可以使用List、List
定义自己的泛型实现,有两种方式:定义泛型类、定义泛型方法,使用的语法结构都为,T非固定要求,只是约定俗成的泛型写法。

//泛型类,成员方法可以允许使用T类型参数和返回值
class MyClass<T> {
 fun method(param: T): T {
 return param
 }
}
//创建泛型类对象,并指定类型
val myClass = MyClass<Int>()
val result = myClass.method(123)
//定义泛型方法
class MyClass {
 fun <T> method(param: T): T {
 return param
 }
}
//调用方法时指定类型
val myClass = MyClass()
val result = myClass.method<Int>(123)
//有类型推理机制
val myClass = MyClass()
val result = myClass.method(123)
//限制类型,通过指定上界的方式对泛型的类型进行约束,表名method方法的泛型指定成数字类型(Int,Float...)
class MyClass {
 fun <T : Number> method(param: T): T {
 return param
 }
}

默认所有泛型都是可空类型,即Any?,若想不为空,只需将泛型的上界手动指定为Any就可以

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
 block()
 return this
}
//参考之前写过的build方法,这样的build方法其实基本与apply无异,但仅作用与StringBuild类上,
//因此我们将其改写为泛型方法
fun <T> T.build(block: T.() -> Unit): T {
 block()
 return this
}

委托

委托是一种设计模式,基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另一个辅助对象去处理。java对于委托没有语言层级的实现。
kotlin支持委托功能,将委托功能分为两种:类委托和委托属性。

  • 类委托
    将一个类的具体实现委托给另一个类去完成。前面用过set,和list相似,但存储数据是无序的,且不能存储重复的数据。set是一个接口,要使用时需要使用它的具体实现类,如HashSet。
    而借助委托模式,我们可以轻松实现自己的实现类,例子如下
//接收了一个HashSet参数,这就相当于一个辅助对象
//在set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,
//等效委托模式
class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
 override val size: Int
 get() = helperSet.size
 override fun contains(element: T) = helperSet.contains(element)
 override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
 override fun isEmpty() = helperSet.isEmpty()
 override fun iterator() = helperSet.iterator()
}

这样写的好处在于,当我们大部分方法实现调用辅助对象中的方法,少部分实现由自己重写,甚至加入自己独有方法,使得MySet成为全新的数据结构类。
但当接口中待实现方法十分多,这样写就十分繁琐,kotlin中可以通过类委托的功能来解决。

kotlin中委托使用关键字为 by

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
//借助类委托后,仅需对需要的方法进行单独重写,其他方法仍可享受类委托带来的便利
 fun helloWorld() = println("Hello World")
 override fun isEmpty() = false
}

委托属性
类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将
一个属性(字段)的具体实现委托给另一个类去完成。

class MyClass {
 var p by Delegate()
}

当调用p属性的时候会自动调用Delegate类的getValue()方法,给p属性赋值时自动调用Delgate类的setValue()方法。
因此,我们得对Delegate类进行具体实现

class Delegate {
 var propValue: Any? = null
 //参数1说明delegate类的委托功能可在什么类中使用,
 //参数2是kotlin中的一个属性操作类,用于或许各种属性相关的值
 operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
 return propValue
 }
 //参数3是具体赋值给委托属性的值,当声明为val,setValue方法则不需要实现
 operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
 propValue = value
 }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值