提高应用开发效率的10个技巧
1. 开发篇
1.灵活运用 CountDownLatch & CyclicBarrier & Semaphore
车载应用的开发中我们会经常遇到各种并发上问题,灵活运用各种线程同步工具,可以显著提高我们处理并发问题时的效率。我们常常把CountDownLatch & CyclicBarrier & Semaphore并称为并发控制的三剑客,在一般APP开发中CountDownLatch和CyclicBarrier相对常用一些。
CountDownLatch
CountDownLatch 它允许一个或多个线程等待其他线程执行完毕后再执行。以下是它的官方介绍:
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
CountDownLatch
用给定的计数初始化。await
方法阻塞,直到由于countDown()
方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await
调用立即返回。 这是一个一次性的现象 - 计数无法重置。 如果需要重置计数的版本,请考虑使用CyclicBarrier
。
它有如下几个方法
//调用await()方法的线程会被挂起,直到count值为0才会被唤醒继续执行
public void await() throws InterruptedException { };
//和await()类似,可以设定一个超时。在等待设定的时间后,即使count不等于0也会被唤醒
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown(){};
CounDownLatch
可以胜任的场景很多,常见例如:我们需要同时从不同的途径获取多个不同的值,等这些值全部取到之后才能再做计算得出期望的结果。
但是,这里列举一个车载应用开发中比较头疼的场景,与Service进行跨进程通信需要先判断Service是否绑定上,否则不能执行数据的请求等操作,有时候我们会Service是否绑定上的回调放在ViewModel中,但这实际上破坏了MVVM架构的设计思想。这时候我们可以考虑使用CountDownLatch
来优化。
首先所有与通过xxxManger与Service进行通信都要调用await(),进入等待状态,如果与Service建立了连接,则在onServiceConnected
方法中将计数器减一,此时所有等待的线程都会进入执行状态。
private CountDownLatch mLatch = new CountDownLatch(1);
CarManager.init(AppGlobal.getApplication(), new ServiceConnectListener() {
@Override
public void onServiceConnected(BaseManager baseManager) {
LogUtils.logI(TAG, "[onServiceConnected]");
mHvacManager = (HvacManager) baseManager;
mLatch.countDown();
}
@Override
public void onServiceDisconnected() {
LogUtils.logI(TAG, "[onServiceDisconnected]");
mLatch = new CountDownLatch(1);
// 重新绑定的逻辑省略
}
});
// 需要放在子线程中调用,否则会block主线程
public HavcManager getHvacManager() {
try {
mLatch.await();
} catch (InterruptedException exception) {
LogUtils.logE(TAG, exception.toString());
}
return mHvacManager;
}
上面是一个车载应用开发中很常见的情景,但对于AIDL SDK的使用我个人更建议这种直接把连接逻辑封装在SDK中的写法 Android 车载应用开发与分析 (4)- 编写基于AIDL 的 SDK。上面的方法适合用来优化旧的代码。
CyclicBarrier
CyclicBarrier 允许一组线程全部等待彼此达到共同屏障点之后,在继续执行另一步操作。以下是它的官方介绍:
循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。
它得用法也很简单,一个形象的例子就是集齐5颗龙珠就可以召唤神龙。调用await()方法后,即表示线程到达屏障点,等待其它线程到达屏障点,所有线程都到达屏障点后,CyclicBarrier初始化时传入的runnable会先执行,之后所有到达屏障点的线程都被唤醒。
val barrier = CyclicBarrier(5) {
Log.e("TAG", "召唤神龙!")
}
for (i in 1..5) {
Thread{
Log.e("TAG", "收集到第${i}颗龙珠")
barrier.await()
Log.e("TAG", "再次集齐,第${i}颗龙珠!")
barrier.await()
}.start()
}
与CountDownLatch不同的是,CyclicBarrier如上面打印的日志展示的那样,
await()
可以被多次调用,每次调用都会产生新的阻塞。而CountDownLatch则不行,在计数器为0后,所有await()
方法都会立即返回,不会产生阻塞。
Semaphore
信号量,主要就是是用来控制并发线程的数量,应用开发不太常用,了解有这么个东西即可。
信号量,在概念上,信号量维持一组许可证。如果有必要,每个
acquire()
都会阻塞,直到许可证可用,然后才能使用它。每个release()
添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象;Semaphore
只保留可用数量的计数,并相应地执行。
信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
下面的代码演示了信号量是如何控制线程访问的。
// 线程池
val exec: ExecutorService = Executors.newCachedThreadPool()
// 只能2个线程同时访问
val semp = Semaphore(2)
for (index in 0..5) {
val run = Runnable {
// 获取许可
semp.acquire()
Log.d("TAG","${index}号客人就餐")
TimeUnit.SECONDS.sleep(2)
// 访问完后,释放
semp.release()
Log.d("TAG","${index}号客人就餐完毕,正在退出")
}
exec.execute(run)
}
2. 更灵活的方式获取Context
有些模块开发时并不持有Activity或Application,通常做法是在暴露一个接口由外部传入Context,但是这会增加接口使用方的负担。运用反射可以让我们在任何时候以更灵活的方式获取到应用的Context。
public class AppGlobal {
private static final String CLASS_FOR_NAME = "android.app.ActivityThread";
private static final String CURRENT_APPLICATION = "currentApplication";
private static final String GET_INITIAL_APPLICATION = "getInitialApplication";
private static Application sApplication;
/**
* Get application.
*
* @return application context.
*/
public static Application getApplication() {
if (sApplication != null) {
return sApplication;
}
try {
Class atClass = Class.forName(CLASS_FOR_NAME);
Method method = atClass.getDeclaredMethod(CURRENT_APPLICATION);
method.setAccessible(true);
sApplication = (Application) method.invoke(null);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException | ClassNotFoundException exception) {
LogUtils.logE(TAG, "exception:" + exception.toString());
}
if (sApplication != null) {
return sApplication;
}
try {
Class atClass = Class.forName(CLASS_FOR_NAME);
Method method = atClass.getDeclaredMethod(GET_INITIAL_APPLICATION);
method.setAccessible(true);
sApplication = (Application) method.invoke(null);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException | ClassNotFoundException exception) {
LogUtils.logE(TAG, "exception:" + exception.toString());
}
return sApplication;
}
}
3. 更优雅的方式关闭资源和数据流
先来看这样一段被Coverity扫描出的问题代码
try {
FileInputStream inputStream = new FileInputStream(file);
// do something.
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
这段代码的问题很明显,如果do something的业务处理时发生异常,那么数据流并不会被关闭,从而出现内存泄露,所以需要在finally语句块中关闭数据流或资源。
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
// do something.
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
但是上面的写法太繁琐了,还需要额外再写一个try语句块。在JDK1.7之后,推荐下面的写法
try (FileInputStream inputStream = new FileInputStream(file)) {
// do something.
} catch (Exception e) {
e.printStackTrace();
}
将需要关闭的数据流和资源放在try()
中,这样资源会在使用完毕后自动关闭,而不需要开发者手动关闭。
4. Room数据库更简便的使用方式
Room是Android Jetpack中提供的一个重要的数据库组件,常规的用法中,我们会以如下方式来定义数据表,每定义一个Entity表示会产生一张数据表。
@Entity
public class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
}
这种方式有个弊端,当应用升级时如果数据表的字段有增减,就会出现如下两种报错
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
处理错误的方式也很简单,增加一个版本迁移的策略,但随着数据表的增加,我们需要处理逻辑也会变得更多。
Room.databaseBuilder(AppGlobal.getApplication(), CacheDatabase.class, DATABASE_NAME)
.allowMainThreadQueries()
.addMigrations(new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 处理迁移逻辑
}
})
.build();
在车载应用开发中,如果数据表的数据量较少时,我们只需要定义一张表,包含一个key和value,value中用来存储序列化后的数据
@Entity(tableName = "table_cache")
public class CacheEntity {
@PrimaryKey
@NonNull
@ColumnInfo(name = "key")
public String mKey;
@ColumnInfo(name = "data")
public byte[] mData;
public CacheEntity(@NonNull String key, byte[] data) {
mKey = key;
mData = data;
}
@Override
public String toString() {
return "CacheEntity{ mKey='" + mKey + '\''
+ ", mData=" + Arrays.toString(mData) + '}';
}
}
待存储的数据需要实现Serializable接口,以支持序列化
public class Entity implements Serializable {
private byte mReason;
private long mDuration;
public byte getReason() {
return mReason;
}
public void setReason(byte reason) {
mReason = reason;
}
public long getDuration() {
return mDuration;
}
public void setDuration(long duration) {
mDuration = duration;
}
}
数据的存储与获取参考下面的方法
@WorkerThread
public static boolean saveData(@NonNull String key, @NonNull T data) {
LogUtils.logI(TAG, "saveData:" + key + " data:" + data.hashCode());
CacheEntity cacheEntity = new CacheEntity(key, toByteArray(data));
long result = CacheDatabase.get().getCacheDao().saveData(cacheEntity);
LogUtils.logI(TAG, "saveData result:" + result);
return result > 0;
}
@WorkerThread
public static T getData(@NonNull String key) {
LogUtils.logI(TAG, "getData:" + key);
CacheEntity entity = CacheDatabase.get().getCacheDao().getData(key);
if (entity == null) {
LogUtils.logI(TAG, "getData result: null");
return null;
} else {
LogUtils.logI(TAG, "getData result:" + entity);
return (T) toObject(entity.mData);
}
}
/**
* Object change to byte array.
*
* @param obj obj
* @param T
* @return byte array
*/
public static byte[] toByteArray(T obj) {
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(arrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
return arrayOutputStream.toByteArray();
} catch (IOException exception) {
LogUtils.logE(TAG, exception.toString());
}
return null;
}
/**
* Byte array change to Object.
*
* @param data byte array
* @return object
*/
public static Object toObject(byte[] data) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
try {
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
return objectInputStream.readObject();
} catch (IOException | ClassNotFoundException ex) {
LogUtils.logE(TAG, ex.toString());
}
return null;
}
改进后,应用中的数据表如下图所示,所有的数据都会被序列化存储在data字段中,即使data中的数据字段发生了增删,通常也不会导致程序崩溃。
5. 使用新版的Fragment
在AndroidX中Fragment的使用方式已经发生比较大的变化,现在我们可以在Fragment的构造方法中可以直接传入布局的id即可完成Fragment的布局。
class Example1Fragment(val param1: String, val param2: String) :
Fragment(R.layout.fragment_example) {
}
同时Google建议使用使用FragmentContainerView
替代FrameLayout
作为Fragment的容器。
更多内容可以参考:Android车载应用开发与分析(番外)- 2022年Fragment使用解析(上)
工具篇
1. OK,Gradle
在AndroidStudio中引入第三方框架依赖总是很让人崩溃,因为根本记不住完整的依赖名,每次引入新的依赖都要上github,但是因为公司网络限制访问github极慢。有个这款插件,只要记住关键词就可以很便捷地引入依赖。
2. SpotBugs
在新版的AndroidStudio中FindBugs已经无法使用了,取而代之的是SpotBugs,使用SpotBugs可以扫描出代码中缺陷,增强代码的健壮性。
ADB 篇
1. 扩大日志缓冲区
Android系统的开发过程中经常会出现 read: unexpected EOF! ,使用-G指令可以临时扩大日志的缓冲区的大小
adb logcat -G 10m
也可以在开发者模式中永久的扩大日志缓冲区
2. 查看当前获取焦点的应用信息
使用下面的指令可以查看处于当前栈顶的Window
adb shell dumpsys window|grep mCurrentFocus
3. 截屏&录屏
截屏和录屏是一个在调试时非常有用的一个指令,不过在某些设备上无法使用。
// 抓取屏幕
adb shell screencap /sdcard/screen.png
// 录制屏幕
adb shell screenrecord /sdcard/hello.mp4