Android学习第十天—跨程序共享数据(探究内容提供器)

其主要目的就是实现跨程序的数据共享,下面进行简单介绍。

内容提供器简介

   重要用于不同应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。
   在正式开始学习之前我们要学习一个重点—Android中的权限的问题,不光是现在在以后的开发过程中我们也会经常使用到权限。

运行时的权限

Android权限机制解析
在Android中国权限分为两种一种是普通权限即不会涉及到你自己的隐私安全等问题的,这类权限系统会自动帮你授权,另一类是危险权限这类权限可能存在危害你的个人隐私等问题所以我们必须要用户手动授权,以上就是大概的权限的了解。
在程序运行时申请权限
我们将以申请打电话这个权限为例子进行简单讲解,在运行时申请权限。
在Android6.0之前拨打电话的功能其实十分简单我们会先在layout中创建一个Button按钮,然后再MainActivity中实现功能,实现功能代码如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button)findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    Intent intent = new Intent(Intent.ACTION_CALL);
                    intent.setData(Uri.parse("tel:10086"));
                    startActivity(intent);
                }catch (SecurityException e){
                    e.printStackTrace();
                }
            }
        });
    }
}

可以看到我们在点击事件当中,创建了一个隐式的Intent,Intent的action指定为Intent。ACTION_CALL,这是一个系统内置的打电话的动作,然后再data中指定了协议是tel,号码是10086,因为这个是直接拨打电话是需要使用话费的所以我们需要申请权限,但是为了防止系统崩溃,将所有的代码放在了异常捕获中。
接下来修改AndroidMainfest文件,其声明如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.pc123.runtimepermissiontest">
    <uses-permission android:name="android.permission.CALL_PHONE"/>//加入这一句代码
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
    若我们在高于android6.0的版本运行就会什么都没有,这是因为我们在使用危险权限时需要进行权限的申请,所以我们需要修改。修改的MainActivity中的代码;首先我们需要判断用户是否给我们授权了,借助ContextCompat.checkSelPermission()方法,checkSelPermission()方法接受两个参数,第一个是Context,第二个是具体的权限的名称比如打电话的权限的名称就是Mainfest.permission.CALL_PHONE,然后我们使用的方法的返回值和PackageManager.PERMISSION_GRANTED作比较,相等说明用户已经授权,不等就是没有授权;
    若已经授权那么我们直接去执行拨打电话的逻辑就行了;若没有授权,我们需要调用ActivityCompat.requestPermissions()方法来向用户申请授权,requestPermissions()里面接受三个参数,第一个是Activity的实例,第二个是一个String数组,我们把申请的权限名称放在数组中,第三个是请求码,只要是唯一值就行,这里传入1。
    在调用完requestPermissions()方法之后会弹出选择框,可以选择同意或者不同意,不论哪种结果,最终都会回归到调用onRequestPremissionResult()方法中,授权的结果会封装在grantResults参数中。
    以上就是大概的思维逻辑,这对我们之后的学习很有帮助,希望我们都能牢牢记住。

访问其他程序中的数据

    内容提供器的方法一般有两种,一种是利用现有的内容提供器来读取和操作相应的程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。
    Android系统中自带的短信、媒体库等程序都提供了类似的接口,我们可以加以利用来实现更好的功能,下面我们就来看看内容提供器到底是怎样工作的。

ContentResolver的基本用法
对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助 ContentResolve 类,可以通过 Context 中的 getContentResolver() 方法获取到该类的实例。 ContentResolver 中提供了一系列的方法用于对数据进行 CRUD 操作,其中 insert()方法用于 添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数 据。有没有似曾相识的感觉?没错,SQLiteDatabase 中也是使用的这几个方法来进行 CRUD 操作的,只不过它们在方法参数上稍微有一些区别。

不同于 SQLiteDatabase,ContentResolver 中的增删改查方法都是不接收表名参数的,而 是使用一个 Uri 参数代替,这个参数被称为内容 URI。内容 URI 给内容提供器中的数据建立 了唯一标识符,它主要由两部分组成,权限(authority)和路径(path)。权限是用于对不同 的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个 程序的包名是 com.example.app ,那么该程序对应的权限就可以命名为 com.example.app. provider。路径则是用于对同一应用程序中不同的表做区分的,通常都会添加到权限的后面。 比如某个程序的数据库里存在两张表,table1 和 table2,这时就可以将路径分别命名为/table1 和/table2,然后把权限和路径进行组合,内容 URI 就变成了 com.example.app.provider/table1 和 com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容 URI,我们还需要在字符串的头部加上协议声明。因此,内容 URI 最标准的格式写法如下:

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

有没有发现,内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数 据。也正是因此,ContentResolver 中的增删改查方法才都接收 Uri 对象作为参数,因为使用 表名的话系统将无法得知我们期望访问的是哪个应用程序里的表。

在得到了内容 URI 字符串之后,我们还需要将它解析成 Uri 对象才可以作为参数传入。 解析的方法也相当简单,代码如下所示:

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

只需要调用 Uri.parse()方法,就可以将内容 URI 字符串解析成 Uri 对象了。
现在我们就可以使用这个 Uri 对象来查询 table1 表中的数据了,代码如下所示:

Cursor cursor = getContentResolver().query(uri,projection, selection, selectionArgs, sortOrder);

这些参数和 SQLiteDatabase 中 query()方法里的参数很像,但总体来说要简单一些,毕 竟这是在访问其他程序中的数据,没必要构建过于复杂的查询语句。下表对使用到的这部分 参数进行了详细的解释。
这里写图片描述
查询完成后返回的仍然是一个 Cursor 对象,这时我们就可以将数据从 Cursor 对象中逐

个读取出来了。读取的思路仍然是通过移动游标的位置来遍历 Cursor 的所有行,然后再取出 每一行中相应列的数据,代码如下所示:

if (cursor != null) {

    while (cursor.moveToNext()) {

        String column1 = cursor.getString(cursor.getColumnIndex("column1"));

        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));

    }

cursor.close();

}

掌握了最难的查询操作,剩下的增加、修改、删除操作就更不在话下了。我们先来看看 如何向 table1 表中添加一条数据,代码如下所示:

ContentValues values = new ContentValues(); values.put("column1", "text"); values.put("column2", 1); getContentResolver().insert(uri, values);

可以看到,仍然是将待添加的数据组装到 ContentValues 中,然后调用 ContentResolver的 insert()方法,将 Uri 和 ContentValues 作为参数传入即可。
现 在 如 果 我 们 想 要 更 新 这 条 新 添 加 的 数 据 , 把 column1 的 值 清 空 , 可 以 借 助
ContentResolver 的 update()方法实现,代码如下所示:

ContentValues values = new ContentValues();

values.put("column1", "");

getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new

String[] {"text", "1"});

注意上述代码使用了 selection 和 selectionArgs 参数来对想要更新的数据进行约束,以防 止所有的行都会受影响。

最后,可以调用 ContentResolver 的 delete()方法将这条数据删除掉,代码如下所示:

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

到这里为止,我们就把 ContentResolver 中的增删改查方法全部学完了,所需特别注 意的就只是uri 这个参数,因为好多东西我们在上一章学习数据库时都见过。

创建自己的内容提供器

注:以下内容来自
作者:TTTqiu
链接:https://www.jianshu.com/p/d3479f9ff85a
來源:简书
因本人小白,故找到了其他大神的神作,望日后需要时可以勤加查看。

新建 MyProvider 继承自 ContentProvider。复写几个方法。
借助 UriMatcher 匹配内容 URI,得知传入的 Uri 对象是想访问哪张表中的哪条数据。
在 MyProvider 的几个方法中用 switch 判断 uriMatcher.match(uri),根据不同 Uri 的目的,把目的定位到相应表或行。
明确目的后,具体靠对 SQLiteDatabase 进行操作完成增删改查。
记住要在 AndroidManifest.xml 里注册。
1. 新建 MyProvider 继承自 ContentProvider。复写几个方法。

public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
        String[] selectionArgs) {
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }
}
  1. onCreate()

初始化内容提供器的时候调用,通常会在这里完成对数据库的创建和升级等操作.
返回 true 表示内容提供器初始化成功,返回 false 则表示失败。
注意,只有当存在 ContentResolver 尝试访问我们程序中的数据时,内容提供器才会被初始化。
2. query()

从内容提供器中查询数据。
使用 uri 参数来确定查询哪张表,projection 参数用于确定查询哪些列,selection 和 selectionArgs 参数用于约束查询哪些行,sortOrder 参数用于对结果进行排序。
查询的结果存放在 Cursor 对象中返回。
3. insert()

向内容提供器中添加一条数据。
使用 uri 参数来确定要添加到的表,待添加的数据保存在 values 参数中。
添加完成后,返回一个用于表示这条新记录的 URI。
4. update()

更新内容提供器中已有的数据。
使用 uri 参数来确定更新哪一张表中的数据,新数据保存在 values 参数中,selection 和 selectionArgs 参数用于约束更新哪些行。
受影响的行数将作为返回值返回。
5. delete()

从内容提供器中删除数据。
使用 uri 参数来确定删除哪一张表中的数据,selection 和 selectionArgs 参数用于约束删除哪些行。
被删除的行数将作为返回值返回。
6. getType()

根据传入的内容 URI 来返回相应的 MIME 类型。
一个内容 URI 所对应的 MIME 字符串主要由三部分组分:
必须以 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() 逻辑 :

public class MyProvider extends ContentProvider {
......
   @Override
   public String getType(Uri uri) {
       switch (uriMatcher.match(uri)) {
           case TABLE1_DIR:
               return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
           case TABLE1_ITEM:
               return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
           case TABLE2_DIR:
               return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
           case TABLE2_ITEM:
               return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
           default:
               break;
       }
       return null;
   }
}
  1. 借助 UriMatcher 匹配内容 URI,得知传入的 Uri 对象是想访问哪张表中的哪条数据。

能够匹配任意表的内容 URI:

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

能够匹配 table1 表中任意一行数据的内容 URI:

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

UriMatcher 中提供了一个 addURI() 方法,接收三个参数,可以分别把权限、路径和一个自定义代码传进去。
当调用 UriMatcher 的 match() 方法时,就可以将一个 Uri 对象传入,返
回值是某个能够匹配这个 Uri 对象所对应的自定义代码。
利用这个代码,我们就可以判断出调用方期望访问的是哪张表或哪一行中的数据了。

public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;

private static UriMatcher uriMatcher;

static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
    uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);
    uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_ITEM);
    uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);
}
  1. 在 MyProvider 的几个方法中用 switch 判断 uriMatcher.match(uri),根据不同 Uri 的目的,把目的定位到相应表或行。
public class MyProvider extends ContentProvider {
    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;

    private static UriMatcher uriMatcher;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
        uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);
        uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_ITEM);
        uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);
    }
......
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                // 查询table1表中的所有数据
                break;
            case TABLE1_ITEM:
                // 查询table1表中的单条数据
                break;
            case TABLE2_DIR:
                // 查询table2表中的所有数据
                break;
            case TABLE2_ITEM:
                // 查询table2表中的单条数据
                break;
            default:
                break;
        }
......
    }
......
}

安全问题:

所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行的,而我们当然不可能向 UriMatcher 中添加隐私数据的 URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。

  1. 明确目的后,具体靠对 SQLiteDatabase 进行操作完成增删改查。
public class DatabaseProvider extends ContentProvider {
    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM = 3;
    public static final String AUTHORITY = "com.example.databasetest.provider";
    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public boolean onCreate() {dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
        // 查询数据
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                cursor = db.query("Book", projection, "id = ?", new String[]{ bookId }, null, null, sortOrder);
                break;
            case CATEGORY_DIR:
                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("Category", projection, "id = ?", new String[] { categoryId }, null, null, sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // 添加数据
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("Book", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("Category", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        // 更新数据
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updatedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                updatedRows = db.update("Book", values, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1); 
                updatedRows = db.update("Book", values, "id = ?", new String[] { bookId });
                break;
            case CATEGORY_DIR:
                updatedRows = db.update("Category", values, selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId });
                break;
            default:
                break;
        }
        return updatedRows;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 删除数据
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                deletedRows = db.delete("Book", selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
                break;
            case CATEGORY_DIR:
                deletedRows = db.delete("Category", selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Category", "id = ?", new String[] { categoryId });
                break;
            default:
                break;
        }
        return deletedRows;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.category";
        }
        return null;
    }
}

getPathSegments() 方法
它会将内容 URI 权限之后的部分以 “/” 符号进行分割,并把分割后的结果放入到一个字符串列表中,这个列表的第 0 个位置存放的就是路径,第 1 个位置存放的就是 id。
5. 记住要在 AndroidManifest.xml 里注册。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.databasetest"
    android:versionCode="1"
    android:versionName="1.0" >
......
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
......
        <provider
            android:name="com.example.databasetest.DatabaseProvider"
            android:authorities="com.example.databasetest.provider" >
        </provider>

    </application>
</manifest>

二、使用自己创建的内容提供器

public class MainActivity extends Activity {
    private String newId;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 添加数据
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
                ContentValues values = new ContentValues();
                values.put("name", "A Clash of Kings");
                values.put("author", "George Martin");
                values.put("pages", 1040);
                values.put("price", 22.85);
                Uri newUri = getContentResolver().insert(uri, values);
                newId = newUri.getPathSegments().get(1);
            }
        });
        Button queryData = (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 查询数据
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
                Cursor cursor = getContentResolver().query(uri, null, null, null, null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.
                        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);
                    }
                    cursor.close();
                }
            }
        });
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 更新数据
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
                ContentValues values = new ContentValues();
                values.put("name", "A Storm of Swords");
                values.put("pages", 1216);
                values.put("price", 24.05);
                getContentResolver().update(uri, values, null, null);
            }
        });
        Button deleteData = (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new OnClickListener() {
            @Override
                public void onClick(View v) {
                // 删除数据
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
                getContentResolver().delete(uri, null, null);
            }
        });
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

li_jeremy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值