目录
实现步骤
代码分析
onCreate
insert
query
ContextHolder
Cursor
作用与用法
基本步骤:
可能的面试题:为什么使用Cursor?
为什么使用Cursor
使用Cursor的好处
静态内部类实现单例模式
AnndroidManifest.xml配置信息
注释的意义
Room持久化数据库
Room的核心组件
Room的主要功能及优势
TripDAO
Context避免内存泄漏:
实现步骤
1、定义URI
定义一个静态的URI 常量指向数据的位置
public static final Uri DATA_URI = Uri.parse("D:/database");
2、创建一个类继承自ContentProvider,实现必要的CRUD(新建,读取,更新,删除操作)
重写以下方法:
- onCreate():初始化数据库及其他资源
- query(),insert(),delete()和update():分别处理对数据的不同操作。每个方法都接收一个URI参数,该参数用于确定所请求的操作类型和目标数据。
- geType():返回给定的URI内容类型。通常需要根据传入的URI返回响应的MIME类型
3. 配置AndroidManifest.xml
为了让系统知道你的ContentProvider,需要在AndroidManifest.xml文件中注册它。这通常是在<application>标签内完成的:
<providerandroid:name=".TripDataProvider"android:authorities="my.application.TripDataProvider"android:exported="false">
</provider>
android:name: 指向你的ContentProvider类。
android:authorities: 应匹配你在代码中定义的AUTHORITY。
android:exported: 如果你想让其他应用程序也能访问这个提供者,可以将其设置为true。
4. 使用URI进行数据操作
通过ContentResolver对象来执行各种数据操作。
查询所有行程:
Cursor cursor = getContentResolver().query(TripDataProvider.TRIP_DATA_URI, null, null, null, null);
插入新的行程记录:
ContentValues values = new ContentValues();// 设置values...Uri newUri = getContentResolver().insert(TripDataProvider.TRIP_DATA_URI, values);
删除某个ID的行程记录:
String selection = "id=?"; // 删除条件
String[] selectionArgs = {"1"}; // 条件参数,假设删除id为1的行程
int rowsDeleted = getContentResolver().delete(TripDataProvider.TRIP_DATA_URI, selection, selectionArgs);
更新某个行程的信息:
ContentValues updateValues = new ContentValues();// 设置updateValues...String selectionForUpdate = "id=?";String[] selectionArgsForUpdate = {"1"};int updatedRows = getContentResolver().update(TripDataProvider.TRIP_DATA_URI, updateValues, selectionForUpdate, selectionArgsForUpdate);
通过这种方式,就可以用ContentProvider实现跨应用组件的数据共享,并且方便地管理数据。
代码分析
onCreate
@Overridepublic boolean onCreate() {mContext = getContext();mDataBase = TripDatabase.getInstance(mContext);ContextHolder.getInstance().setContext(mContext);return true;}
1. 获取Context 上下文
mContext = getContext();
此处调用了getContext()方法来获取当前的Context。对于ContentProvider而言,getContext()返回的是该提供者运行于的应用程序的上下文环境。这个上下文是ContentProvider与应用程序其他部分进行交互的关键。
2. 初始化数据库
mDataBase = TripDatabase.getInstance(mContext);
此处使用从getContext()获得的Context来初始化TripDatabase实例。TripDatabase是基于Room持久化库构建的数据库类,详见后文。
getInstance(mContext)是一个单例模式的方法,确保在整个应用生命周期内只存在一个数据库实例。这样做有助于节省资源并保持数据的一致性。
3. 设置ContextHolder的Context
ContextHolder.getInstance().setContext(mContext);
调用ContextHolder的getInstance()方法获取其单例实例,然后通过setContext(mContext)方法将之前获取的mContext 上下文对象传递给ContextHolder。 这一步是为了在整个应用程序中共享这个mContext,特别是对于那些可能需要访问应用程序上下文但无法直接获得它的组件来说非常有用。
小结:
获取上下文
初始化数据库对象,根据上下文来获取数据
更新上下文,更新上下文的全局持有器
insert
@Overridepublic Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {int uriType = S_URI_MATCHER.match(uri);if (uriType == TRIP_DATA_PROVIDER) {TripInfo info = formatContentValueToInfo(values);if (info != null) {long id = mDataBase.tripDAO().insertTrip(info);mContext.getContentResolver().notifyChange(uri, null);if (id > 0) {Uri newUri = Uri.parse(uri.toString() + "/" + id + "|" + OPERATION_TYPE_ADD);mContext.getContentResolver().notifyChange(newUri, null);return ContentUris.withAppendedId(uri, id);}}}return null;}
该方法主要用于向数据库中插入一条新的行程(Trip)记录,并通过Uri通知数据的变化。下面是对这段代码的具体分析:
插入操作:该方法接收一个Uri对象和一个可选的ContentValues对象作为参数,代表要插入的数据。
URI匹配:首先使用S_URI_MATCHER.match(uri)来确定传入的Uri类型是否与预定义的TRIP_DATA_PROVIDER相匹配。这一步是为了确保请求的URI是针对行程数据的操作。
数据转换:如果匹配成功,则调用formatContentValueToInfo(values)将ContentValues对象转换为自定义的TripInfo对象,用于后续的数据处理。
数据库插入:使用mDataBase.tripDAO().insertTrip(info)将TripInfo对象插入到数据库中,并返回新插入记录的ID。
tripDao是一个抽象接口,后文有具体的分析
Uri newUri = Uri.parse(uri.toString() + "/" + id + "|" + OPERATION_TYPE_ADD);mContext.getContentResolver().notifyChange(newUri, null);
通知变更:如果插入成功(即返回的ID大于0),则构建一个新的Uri对象,这个新Uri是在原Uri的基础上附加了新记录的ID以及一个操作类型标识(在这个例子中是OPERATION_TYPE_ADD)。然后,分别对原始Uri和新生成的Uri调notifyChange方法,以通知监听这些Uri的观察者数据已经发生变化。
返回结果:最后,如果插入成功,使用ContentUris.withAppendedId(uri, id)构造并返回一个新的Uri,它包含了新插入记录的ID;如果插入失败或info为null,则返回null。
小结:
1、URI匹配:检查传入URI是否对应于形成数据的操作
2、数据类型转换 数据层和业务逻辑层分离
TripInfo info = formatContentValueToInfo(values);
3、 数据插入与通知:对原始Uri进行变更通知,还创建了一个更具体的`Uri`来反映刚刚添加的数据项,并同样发出变更通知。
4. 返回值:如果插入成功返回的Uri包含了新记录的ID,这使得调用者能够直接获取新记录的位置。若插入失败,则返回null;
query
@Nullable@Overridepublic Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {int uriType = S_URI_MATCHER.match(uri);Log.e(TAG, "uri path:" + uri.toString());Cursor cursor = null;if (uriType == TRIP_DATA_PROVIDER) {if (selectionArgs == null) {cursor = mDataBase.tripDAO().findTrips();} else {if (selectionArgs.length >= 1) {String id = selectionArgs[0];cursor = mDataBase.tripDAO().findTripById(Integer.parseInt(id));}}}return cursor;}@Nullable@Overridepublic String getType(@NonNull Uri uri) {return null;}
这query()方法实现,用于根据给定的URI查询数据库并返回结果。
方法分析 :
1. 参数解释 Uri uri: 标识要访问的数据集。
String[] projection: 要返回的列名数组。如果为null,则返回所有列。
String selection: 一个过滤条件,类似于SQL中的WHERE子句。
String[] selectionArgs: 上述selection中占位符的实际值。
String sortOrder: 排序顺序,类似于SQL中的ORDER BY子句。
2. 匹配URI类型
int uriType = S_URI_MATCHER.match(uri);
使用UriMatcher来确定传入的URI对应的资源类型。
在这个例子中,通过S_URI_MATCHER.match(uri)来匹配URI,并根据其类型执行相应的操作。
3. 根据URI类型进行查询
如果uriType等于TRIP_DATA_PROVIDER,表示请求的是行程数据:
如果没有提供selectionArgs(即查询所有行程),则调用mDataBase.tripDAO().findTrips()获取所有行程记录。
如果提供了selectionArgs并且至少包含一个元素,则认为是要根据ID查询特定行程,因此调用mDataBase.tripDAO().findTripById(Integer.parseInt(id))。
5. 返回查询结果
将查询结果封装在Cursor对象中返回。
在这个例子中,query()方法返回的Cursor可以被其他组件(如Activity或Fragment)用来展示或进一步处理从数据库检索到的数据。这使得ContentProvider不仅能够提供对数据的安全访问,还能灵活地支持各种数据操作需求。
delete
update 略
ContextHolder
源码:
public class ContextHolder {private ContextHolder() {}private static class InstanceHolder {private static final ContextHolder INSTANCE = new ContextHolder();}public static ContextHolder getInstance() {return InstanceHolder.INSTANCE;}private Context mContext;public Context getContext() {return LauncherApp.getApplication();}public void setContext(Context mContext) {if (!(mContext instanceof Application)) {throw new RuntimeException("context should be Application context to avoid context leak");}this.mContext = mContext;}
}
这段代码定义了一个名为ContextHolder的类,用于在应用程序的不同部分之间共享和管理全局Context。
以下是对该类各个组成部分的详细分析:
单例模式 :
私有构造函数:ContextHolder的构造函数被声明为private,这意味着它不能从外部直接实例化。
这种设计是实现单例模式的一部分,确保整个应用程序中只有一个ContextHolder实例存在。
静态内部类InstanceHolder:通过使用静态内部类InstanceHolder来持有唯一的ContextHolder实例,这是一种懒汉式单例模式的变体。这种方式利用了Java的类加载机制,在第一次调用getInstance()方法时才加载`InstanceHolder`类,从而延迟了`ContextHolder`实例的创建,实现了懒加载的效果。这样做不仅保证了线程安全,而且避免了同步带来的性能开销。
上下文管理
成员变量mContext:用来存储应用程序的上下文(Context),通常是一个Application对象,以避免内存泄漏的风险。 getContext() 方法:返回当前保存的Context。
LauncherApp.getApplication()来返回一个Context。
public Context getContext() { return LauncherApp.getApplication();
}
setContext(Context mContext) 方法:允许设置ContextHolder持有的Context,但仅当传入的Context是Application类型时才允许设置。
这是为了避免由于持有Activity或Service等生命周期较短的Context而导致的内存泄漏问题。
public void setContext(Context mContext) { if (!(mContext instanceof Application)){ throw new RuntimeException("context should be Application context to avoid context leak"); } this.mContext = mContext;
}
小结:ContextHolder的设计目的是为了提供全局安全访问和共享Context的方式。
采用了单例模式和懒加载策略来确保高效且线程安全的实例管理。通过强制要求Context必须是Application类型,有效地防止了因错误地传递Activity或Service导致的内存泄漏问题
Cursor
作用与用法
Cursor是一个接口,允许你遍历结果集中的行和列,并提取所需的数据,它代表了一个由数据库查询返回的结果集,。
基本步骤:
移动到指定位置:使用moveToFirst(), moveToNext(), moveToLast(), 或者 moveToPosition(int position)等方法在结果集中移动指针。
读取数据:一旦定位到某一行,可以使用如getString(columnIndex), getInt(columnIndex), getLong(columnIndex)等方法读取特定列的数据。
注意,这里的columnIndex是指列在查询结果中的索引,而不是列名。
关闭Cursor:完成数据读取后应调用close()方法释放Cursor占用的资源。 例如,假设我们有一个查询结果`Cursor,可以通过以下方式遍历结果集并读取数据:
if (cursor != null) { while (cursor.moveToNext()) { // 假设第一个列是'id',第二个列是'name' int id = cursor.getInt(cursor.getColumnIndexOrThrow("id")); String name = cursor.getString(cursor.getColumnIndexOrThrow("name")); // 处理id和name... } cursor.close(); // 关闭Cursor以释放资源
}
可能的面试题:为什么使用Cursor?
Cursor是Android中用于表示数据库查询结果集的接口。当你执行一个查询操作时,返回的结果通常是一个Cursor对象,它允许你遍历查询结果中的行和列,并提取所需的数据。以下是使用Cursor的原因及其带来的好处:
为什么使用Cursor
1. 高效处理大数据集:
Cursor允许逐行读取数据,而不是一次性将所有数据加载到内存中。这对于处理大量数据特别有用,因为它减少了内存占用并提高了应用的性能。
2.支持复杂查询结果:
Cursor`可以处理复杂的查询结果,包括多表连接、聚合函数等产生的结果集。它不仅限于简单的单表查询。
3. 灵活的数据访问方式:
使用Cursor,你可以根据需要选择要访问的数据列和行,提供了极大的灵活性。例如,你可以只获取特定的几列数据,而不需要加载整个行的所有列。
4. 与ContentProvider无缝集成:
在Android中,ContentProvider经常用来在不同的应用程序或应用组件之间共享数据。Cursor是ContentProvider返回查询结果的标准方式,这使得不同部分之间的数据交换变得简单且一致。
使用Cursor的好处
1. 节省内存:
如前所述,由于Cursor允许逐行读取数据,因此相比于一次性加载所有数据到内存中,它可以大大减少内存消耗。
2. 简化数据处理逻辑:
Cursor提供了一系列便捷的方法来获取不同类型的数据(如getInt(), getString(), getBlob()等),这使得从查询结果中提取数据变得更加直接和容易。
3. 生命周期管理:
Cursor具有自己的生命周期,可以通过调用close()方法手动关闭它,从而释放相关资源。良好的资源管理有助于避免潜在的内存泄漏问题。
4. 支持异步操作:
虽然`Cursor本身并不直接支持异步操作,但结合Loader框架(例如LoaderManager和AsyncTaskLoader)或Room数据库提供的LiveData支持,可以实现非阻塞的数据加载,提升用户体验。
5. 便于UI更新:
当与CursorAdapter结合使用时,Cursor可以直接作为数据源绑定到UI组件上(如ListView或RecyclerView),使得数据显示更加简便。
Java的类加载机制和懒汉式单例模式虽然涉及的概念不同,但它们之间存在一定的联系,尤其是在实现单例模式时如何确保类实例的唯一性和延迟加载方面。下面将详细解释这两个概念,并探讨它们之间的关系。
静态内部类实现单例模式
public class ContextHolder {private ContextHolder() {// 私有构造函数,防止外部实例化}private static class InstanceHolder {// 静态内部类,只有在调用getInstance方法时才会被加载private static final ContextHolder INSTANCE = new ContextHolder();}public static ContextHolder getInstance() {return InstanceHolder.INSTANCE;}// 其他代码...
}
1.、延迟加载,按需加载:懒汉式单例模式利用了Java类加载机制中的延迟加载特性。具体来说,在单例类首次被真正使用之前(例如调用getInstance()`方法),类不会被加载,因此单例对象也不会被实例化。
2、这是一种更加推荐的懒汉式单例模式实现方式,它不仅实现了延迟加载,而且借助了类加载机制的线程安全性。这种方式通过定义一个静态内部类来持有单例对象,在第一次引用该内部类时才会触发类加载及其实例化操作,从而保证了线程安全且高效的单例模式实现。
关键点解析
1. 私有构造函数:通过将构造函数设为private,防止其他类直接使用`new`关键字来实例化该类的对象,这是单例模式的基础。
2. 静态内部类InstanceHolder:仅当调用ContextHolder.getInstance()方法时,`InstanceHolder`类才会被加载。这是因为Java中静态成员(包括静态内部类)遵循“按需加载”的原则——即直到第一次使用时才会加载静态内容。
3. **INSTANCE变量**:在`InstanceHolder`内部定义了一个静态的`INSTANCE`变量,用于保存单例对象。由于静态内部类只会被加载一次,因此保证了`INSTANCE`的唯一性。
4. 线程安全:利用Java类加载机制的特性,确保了多线程环境下创建单例对象的线程安全性,而无需额外的同步处理。
5. 懒加载:这种方式实现了懒加载,即只有当真正需要使用单例对象时(即调用了getInstance()),才会初始化该对象,而不是在类加载时就进行初始化。
载器才会尝试自己加载。这种方法可以有效避免类的重复加载,同时增强了类的安全性。
对于静态内部类实现单例模式来说,正是利用了Java的“按需加载”特性。静态内部类InstanceHolder不会在`ContextHolder`类加载时就被加载,而是延迟到首次调用getInstance()方法时才加载。这不仅保证了单例对象的唯一性和线程安全性,还实现了懒加载,提高了资源利用率。
AnndroidManifest.xml配置信息
应用主要配置了权限声明、保护广播以及应用组件等信息。
以下是对该文件中包含的内容的分析:
1. 权限声明:
android.permission.ACCESS_NETWORK_STATE和 android.permission.INTERNET:允许应用程序访问网络状态和打开网络连接。
android.permission.BLUETOOTH 和 android.permission.BLUETOOTH_ADMIN:允许应用程序配对和连接蓝牙设备,并进行蓝牙管理操作。
android.permission.READ_EXTERNAL_STORAGE 和 android.permission.WRITE_EXTERNAL_STORAGE:允许应用程序读取和写入外部存储。
android.permission.FOREGROUND_SERVICE`:允许应用程序创建前台服务,这在执行长时间运行的任务时非常有用。
android.permission.ACCESS_WIFI_STATE:允许应用程序访问有关Wi-Fi网络的信息。
2. 保护广播(Protected Broadcasts):
应用程序定义了两个受保护的广播,分别是application.ACTION_MAP_RECV和application.ACTION_SCENE_CARD。
这意味着只有具有特定权限的应用才能发送这些广播动作,增强了应用的安全性。
3. Application节点下的配置:
<uses-library android:name="myApplication.framework" />表示此应用依赖于名为myApplication.framework的库,这可能与框架或功能有关,
Content Provider的配置,即TripDataProvider。
注释的意义
注释(Annotations)在编程中用于为代码添加元数据,这些信息可以被编译器、工具或运行时环境读取和使用。它们本身不会改变程序的执行逻辑,但可以影响编译过程、生成文档、进行代码检查等。你提到的几个注释分别具有以下意义:
1. @NonNull: 这个注释通常用来标明一个方法参数、返回值或变量不能为 null。这有助于静态分析工具识别潜在的空指针异常,并提醒开发者确保该元素在所有情况下都不应为空。
2. @Nullable: 与 @NonNull相反,这个注释表示一个方法参数、返回值或变量可能为 `null`。它提示开发者需要处理这种情况,以避免空指针异常或其他错误。
3. @Dao: 这个注释是特定于Android开发中的Room持久化库的一部分。@Dao注释用于标记接口或抽象类作为数据访问对象(DAO)。DAO提供了访问数据库的方法,如插入、删除、查询等操作。Room是一个官方提供的轻量级ORM(对象关系映射)库,旨在帮助开发者更加方便地创建应用的持久层
Room持久化数据库
Room持久化库是Android Jetpack的一部分,它提供了一个抽象层来访问应用程序的SQLite数据库。通过Room,开发者可以更方便地进行数据库操作,同时避免了直接使用SQLite API时的一些复杂性和潜在错误。
Room的核心组件
1. Entity(实体)
实体代表数据库中的表。每个实体类对应一个数据库表。例如,在您的项目中很可能被标记为@Entity注解,并且包含一些字段,这些字段将映射到表中的列。
@Entity(tableName = "trips")
public class TripInfo {@PrimaryKey(autoGenerate = true)private int id;private String name;// 其他字段...
}
2. DAO (Data Access Object)
DAO是定义用于访问数据库的方法接口。通过DAO,您可以轻松执行CRUD(创建、读取、更新、删除)操作。
在TripDAO接口中,定义了多种方法来进行数据操作,比如插入、删除、查询等。
@Dao
public interface TripDAO {@Insert(onConflict = OnConflictStrategy.REPLACE)long insertTrip(TripInfo info);@Query("SELECT * FROM trips WHERE id = :id")Cursor findTripById(int id);// 更多方法...
}
3. Database
数据库持有者是一个继承自RoomDatabase的抽象类。在这个例子中就是TripDatabase。
它使用@Database注解来指定其所关联的实体列表以及数据库版本号,并提供了获取DAO实例的方法。
@Database(entities = {TripInfo.class}, version = 1)public abstract class TripDatabase extends RoomDatabase {public abstract TripDAO tripDAO();private static volatile TripDatabase INSTANCE;public static TripDatabase getInstance(Context context) {if (INSTANCE == null) {synchronized (TripDatabase.class) {if (INSTANCE == null) {INSTANCE = Room.databaseBuilder(context.getApplicationContext(),TripDatabase.class,"trip.db").allowMainThreadQueries().build();}}}return INSTANCE;}}
Room的主要功能及优势
1. 简化数据库操作:Room通过DAO接口自动生成实现,减少了大量样板代码。只需定义接口和方法签名,Room会在编译期生成具体的实现代码。
2. 类型安全与编译时检查:Room在编译时验证SQL查询语句的有效性,有助于减少运行时错误。如果查询语句有误,编译器会立即提示。
3. 支持LiveData和RxJava:Room能够返回LiveData对象,这样当底层数据库发生变化时,UI可以自动更新。此外,也支持RxJava类型如Flowable、Single等。
4. 事务支持:Room允许在一个事务中执行多个操作,确保要么所有操作都成功,要么全部不执行。
5. 异步查询支持:为了避免阻塞主线程,Room默认不允许在主线程上执行数据库查询。不过,您可以通过.allowMainThreadQueries()选项临时关闭这一限制(仅限于测试环境)。
6. 数据库迁移支持:随着应用的发展,数据库结构可能需要更改。Room提供了易于使用的API来处理数据库版本升级和迁移。
Room持久化库极大地简化了Android应用程序中数据库的操作,提高了开发效率并减少了出错的可能性。结合ContentProvider使用,不仅可以优化内部数据管理,还能增强数据共享能力,满足更多应用场景的需求。
TripDAO
public interface TripDAO {@Insert(onConflict = OnConflictStrategy.REPLACE)public long insertTrip(TripInfo info);@Deletepublic int deleteTrip(TripInfo... info);@Query("DELETE FROM trips WHERE id = :id")public int deleteTrip(int id);@Query("DELETE FROM trips WHERE uuid = :uuid")public int deleteTrip(String uuid);@Query("SELECT * FROM trips WHERE id = :id")public Cursor findTripById(int id);@Query("SELECT * FROM trips")public Cursor findTrips();@Query("SELECT * FROM trips")public List<TripInfo> getAllTrips();@Query("SELECT * FROM trips WHERE tripReallyTime >= :startTime and tripReallyTime <= :endTime")public List<TripInfo> getAllTrips(long startTime,long endTime);@Query("SELECT * FROM trips WHERE id = :id")public TripInfo getTripById(int id);@Query("SELECT * FROM trips WHERE UUID = :uuid")public List<TripInfo> getTripById(String uuid);@Updatepublic int updateTrips(TripInfo info);}
这个TripDAO接口定义了一系列的方法,用于对名为`trips`的数据库表进行操作。它使用了Room持久化库提供的注解来简化SQLite数据库的操作。以下是对每个方法及其用途的详细分析:
接口与注解
@Dao: 标记该接口为一个数据访问对象(DAO),这是Room用来标识可以执行数据库操作的类或接口的关键字。
方法:
1. 插入数据
@Insert(onConflict = OnConflictStrategy.REPLACE)public long insertTrip(TripInfo info);
插入一个新的行程记录到trips表中。如果存在冲突(如主键冲突),则替换现有行。
返回值是新插入行的ID(对于自增主键)。
2. 删除数据
根据对象删除
@Deletepublic int deleteTrip(TripInfo... info);
删除传入的一个或多个`TripInfo`对象对应的记录。返回被删除的行数。
根据ID删除
@Query("DELETE FROM trips WHERE id = :id")public int deleteTrip(int id);
根据给定的ID删除特定行程记录。
根据UUID删除
@Query("DELETE FROM trips WHERE uuid = :uuid")public int deleteTrip(String uuid);
3. 查询数据
根据ID查找单个行程
@Query("SELECT * FROM trips WHERE id = :id")public Cursor findTripById(int id);
返回一个Cursor,指向具有指定ID的行程记录。适用于需要逐行读取结果的情况。
查找所有行程
@Query("SELECT * FROM trips")public Cursor findTrips();
总结
该接口提供了一组全面的方法来管理`trips`表的数据,包括插入、删除、查询和更新操作。通过使用Room提供的注解,这些方法能够以简洁的方式实现复杂的数据库操作,减少了手动编写SQL语句的需求,并提供了类型安全性和编译时检查的支持。这使得开发者可以更加专注于业务逻辑而不是底层的数据库细节。此外,支持返回Cursor和List<TripInfo>类型的查询方法,为不同的应用场景提供了灵活性。
Context避免内存泄漏:
长生命周期对象持有短声明周期对象,避免方法,只有当context是Application类型才赋值