专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
郭霖  ·  使用Hilt来协助封装网络请求 ·  3 天前  
鸿洋  ·  一个大型 Android 项目的模块划分哲学 ·  3 天前  
鸿洋  ·  Android H5页面性能分析策略 ·  1 周前  
51好读  ›  专栏  ›  郭霖

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

郭霖  · 公众号  · android  · 2024-11-12 08:00

正文



/   今日科技快讯   /


美国国家航空航天局(NASA)坚称,被困在国际空间站的女宇航员苏尼塔·威廉姆斯(Sunita Williams)安全且健康。10月28日,白宫在排灯节招待会上播放了美国宇航员威廉姆斯的致辞,她看上去精神良好。然而一张9月24日的照片显示威廉姆斯的脸颊凹陷成“锥子脸”,引发公众担忧。


/   作者简介   /


本篇文章转载自萌娃瑜宝爸比的博客,文章主要分享了Android 跨进程+解耦的数据持久化方案,相信会对大家有所帮助!


原文地址:

https://juejin.cn/post/7372396200861499442


/   前言   /


如果提到跨进程你肯定会想到 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 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


    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 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 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 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 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");
        }
    });
}


效果



FQA


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


A:




    "com.xxxx.diagnose" />

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

原创:写给初学者的Jetpack Compose教程,edge-to-edge全面屏体验

Github Copilot 近期的一次重要更新


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注