Android 跨进程+解耦的数据持久化方案

如果提到跨进程你肯定会想到 AIDL , 没错我们确实是频繁使用到 AIDL 去 bind 服来完成跨进程通信。但是 AIDL 有个弊端是如果是跨两个应用之间我们需要互相知道对方的 AIDL 文件,这样我们在 bind 成功后才能知道 Binder 是什么类型有哪些接口:

bindService(intent, new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Log.e(TAG, "onServiceConnected: ");
        //here "IBinder"

    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Log.e(TAG, "onServiceDisconnected: ");

    }
}, Context.BIND_AUTO_CREATE);

另外如果是一些持久化到本地的数据,bind service 的方式也不是最优解。

这种解耦的支持跨进程的持久化存储方案:今天带来 ContentProvider+Room 来给大家出个示例。

ContentProvider 是基于 Uri 的,天然就算是解耦的,不需要有任何的 sdk 或者 aidl 文件依赖就能达到耦合和跨进程。

content://com.xxx.xxx/xxx

是不是有点类似 http:// 或者是 ws:// 闲话不多说直接看示例。

数据库

ContentProvider 可以理解为接口,所有的增删改查都需要有具体的数据库实现,数据库的选型大家可以随意,你可以用原生 sqlite 也可以用 ORM 映射框架。因为笔者曾经对 room 写过一篇文章 Android ROOM 数据库高手秘籍 索性这里我们就用 ROOM。

def room_version = "2.5.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

 

@Entity(tableName = "user_table")
public class User {
    @PrimaryKey(autoGenerate = true)
    private int id;
    @ColumnInfo
    private String name;

    @ColumnInfo
    private int age;

    // Getters and setters...
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    // Convert ContentValues to User object
    public static User fromContentValues(ContentValues values) {
        User user = new User();
        if (values.containsKey("id")) {
            user.setId(values.getAsInteger("id"));
        }
        if (values.containsKey("name")) {
            user.setName(values.getAsString("name"));
        }
        if (values.containsKey("age")) {
            user.setAge(values.getAsInteger("age"));
        }
        return user;
    }
}
@Dao
public interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    long insert(User user);

    @Update
    int update(User user);

    @Delete
    int delete(User user);

    @Query("DELETE FROM user_table")
    int deleteAll();

    @Query("DELETE FROM user_table WHERE id = :userId")
    int deleteById(int userId);

    @Query("SELECT * FROM user_table WHERE id = :userId")
    User getUserById(int userId);

    @Query("SELECT * FROM user_table")
    List<User> getAllUsers();

    @Query("SELECT * FROM user_table WHERE id = :userId")
    Cursor getUserByIdCursor(int userId);

    @Query("SELECT * FROM user_table")
    Cursor getAllUsersCursor();

    @RawQuery
    int checkpoint(SupportSQLiteQuery supportSQLiteQuery);
}
@Database(entities = {Car.class, User.class}, version = 1, exportSchema = false)
public abstract class DiagnoseDatabase extends RoomDatabase {
    private static volatile DiagnoseDatabase INSTANCE;

    public abstract CarDao carDao();

    public abstract UserDao userDao();

    public static DiagnoseDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (DiagnoseDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                                    DiagnoseDatabase.class, "diagnose.db")
                            .fallbackToDestructiveMigration()
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

ContentProvider

<provider
    android:authorities="com.xxx.diagnose"
    android:name="com.xxx.diagnose.provider.DiagnoseContentProvider"
    android:exported="true"/>
public class DiagnoseContentProvider extends ContentProvider {

    public static final String TAG = DiagnoseContentProvider.class.getSimpleName();

    private DiagnoseDatabase db;

    public static final String AUTHORITY = "com.xxx.diagnose";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/users");

    private static final int USERS = 1;
    private static final int USER_ID = 2;

    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        uriMatcher.addURI(AUTHORITY, "users", USERS);
        uriMatcher.addURI(AUTHORITY, "users/#", USER_ID);
    }

    private Handler daoHandler;

    @Override
    public boolean onCreate() {
        initDB();
        HandlerThread handlerThread = new HandlerThread("DaoHandler");
        handlerThread.start();
        daoHandler = new Handler(handlerThread.getLooper());
        return true;
    }

    private void initDB() {
        Log.e(TAG, "initDB");
        db = DiagnoseDatabase.getInstance(getContext());
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        final BlockingQueue<Cursor> cursorBlockingQueue = new ArrayBlockingQueue<>(1);

        daoHandler.post(() -> {
            Cursor cursor = null;
            switch (uriMatcher.match(uri)) {
                case USERS:
                    cursor = db.userDao().getAllUsersCursor();
                    break;
                case USER_ID:
                    long id = ContentUris.parseId(uri);
                    cursor = db.userDao().getUserByIdCursor((int) id);
                    break;
            }
            try {
                cursorBlockingQueue.put(cursor);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.e(TAG, "Interrupted while waiting to put cursor in queue", e);
            }
        });

        try {
            return cursorBlockingQueue.poll(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.e(TAG, "Interrupted while waiting to take cursor from queue", e);
            return null;
        }
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case USERS:
                return "vnd.android.cursor.dir/vnd." + AUTHORITY + ".users";
            case USER_ID:
                return "vnd.android.cursor.item/vnd." + AUTHORITY + ".users";
            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        if (uriMatcher.match(uri) != USERS) {
            throw new IllegalArgumentException("Unsupported URI for insertion: " + uri);
        }

        final BlockingQueue<Uri> resultBlockingQueue = new ArrayBlockingQueue<>(1);

        daoHandler.post(() -> {
            long id = db.userDao().insert(User.fromContentValues(values));
            Uri resultUri = ContentUris.withAppendedId(uri, id);
            try {
                resultBlockingQueue.put(resultUri);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.e(TAG, "Interrupted while waiting to put result in queue", e);
            }
        });

        try {
            return resultBlockingQueue.poll(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.e(TAG, "Interrupted while waiting to take result from queue", e);
            return null;
        }
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        final BlockingQueue<Integer> resultBlockingQueue = new ArrayBlockingQueue<>(1);

        daoHandler.post(() -> {
            int rowsDeleted = 0;
            switch (uriMatcher.match(uri)) {
                case USERS:
                    rowsDeleted = db.userDao().deleteAll();
                    break;
                case USER_ID:
                    long id = ContentUris.parseId(uri);
                    rowsDeleted = db.userDao().deleteById((int) id);
                    break;
            }
            try {
                resultBlockingQueue.put(rowsDeleted);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.e(TAG, "Interrupted while waiting to put result in queue", e);
            }
        });

        try {
            return resultBlockingQueue.poll(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.e(TAG, "Interrupted while waiting to take result from queue", e);
            return 0;
        }
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        if (uriMatcher.match(uri) != USER_ID) {
            throw new IllegalArgumentException("Unsupported URI for update: " + uri);
        }

        long id = ContentUris.parseId(uri);
        User user = User.fromContentValues(values);
        user.setId((int) id);

        final BlockingQueue<Integer> resultBlockingQueue = new ArrayBlockingQueue<>(1);

        daoHandler.post(() -> {
            int rowsUpdated = db.userDao().update(user);
            try {
                resultBlockingQueue.put(rowsUpdated);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.e(TAG, "Interrupted while waiting to put result in queue", e);
            }
        });

        try {
            return resultBlockingQueue.poll(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.e(TAG, "Interrupted while waiting to take result from queue", e);
            return 0;
        }
    }
}

外部使用

public static final String TAG = MainActivity.class.getSimpleName();

private Uri newUserUri;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    findViewById(R.id.insert).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ContentValues values = new ContentValues();
            values.put("name", "John Doe");
            values.put("age", 30);
            newUserUri = getContentResolver().insert(DiagnoseContentProvider.CONTENT_URI, values);
            Log.e(TAG,"insert  : " + newUserUri);
        }
    });

    findViewById(R.id.query_all).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Query all users
            Cursor cursor = getContentResolver().query(DiagnoseContentProvider.CONTENT_URI, null, null, null, null);
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
                    int age = cursor.getInt(cursor.getColumnIndexOrThrow("age"));
                    int id = cursor.getInt(cursor.getColumnIndexOrThrow("id"));
                    // Do something with the user data
                    Log.e(TAG," name : " + name + " age : " + age + " id : " + id);
                }
                cursor.close();
            }
        }
    });

    findViewById(R.id.update).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ContentValues updateValues = new ContentValues();
            updateValues.put("name", "Jane Doe");
            updateValues.put("age", 25);
            getContentResolver().update(newUserUri, updateValues, null, null);

        }
    });

    findViewById(R.id.delete).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Delete a user
            getContentResolver().delete(newUserUri, null, null);
        }
    });

    findViewById(R.id.query_by_id).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Query a user by ID
            int userId = 1; // 假设要查询的用户 ID 为 1
            Uri queryUri = ContentUris.withAppendedId(DiagnoseContentProvider.CONTENT_URI, userId);
            Cursor cursor = getContentResolver().query(queryUri, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
                int age = cursor.getInt(cursor.getColumnIndexOrThrow("age"));
                // Do something with the user data
                Log.e(TAG, " name : " + name + " age : " + age + " id : " + userId);
                cursor.close();
            } else {
                Log.e(TAG, "User not found with ID: " + userId);
            }
        }
    });

    findViewById(R.id.detele_all).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Delete all users
            int rowsDeleted = getContentResolver().delete(DiagnoseContentProvider.CONTENT_URI, null, null);
            Log.e(TAG, "Deleted " + rowsDeleted + " users");
        }
    });
}

效果

image.png

FQA

Q:为什么我跨进程使用 getContentResolver 无法访问到数据,已经检查了 Uri 没有错误?

A:

<!--    由于 Android 11 包的可见性限制,跨进程或者跨应用调用必须在使用端声明 queries 否则会 Failed to find provider info for com.xxxx.diagnose 报错-->
<queries>
    <package android:name="com.xxxx.diagnose" />
</queries>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值