提高应用开发效率的10个技巧

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 提高应用开发效率的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 <T extends Serializable> 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 extends Serializable> 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> T
 * @return byte array
 */
public static <T extends Serializable> 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的容器。

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

更多内容可以参考:Android车载应用开发与分析(番外)- 2022年Fragment使用解析(上)

工具篇

1. OK,Gradle

在AndroidStudio中引入第三方框架依赖总是很让人崩溃,因为根本记不住完整的依赖名,每次引入新的依赖都要上github,但是因为公司网络限制访问github极慢。有个这款插件,只要记住关键词就可以很便捷地引入依赖。


image.png

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
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
XML 数据管理 测试技术
深入探索软件自动化测试框架的设计与实现
【4月更文挑战第26天】 随着软件开发周期不断缩短,传统的手动测试方法已难以满足快速迭代的需求。本文聚焦于自动化测试框架的构建与优化,旨在提供一种高效、可维护且可扩展的软件测试解决方案。文章从自动化测试的必要性出发,详细阐述了自动化测试框架设计的核心要素,包括模块化设计、数据驱动测试以及关键词驱动测试等概念。同时,结合实例分析了如何利用流行的测试工具进行框架搭建,并提出了针对常见问题的创新解决方法。最后,通过案例研究展示了该框架在实际项目中的应用效果和潜在改进空间。
|
10天前
|
监控 jenkins 测试技术
自动化测试框架的构建与实践
【10月更文挑战第40天】在软件开发周期中,测试环节扮演着至关重要的角色。本文将引导你了解如何构建一个高效的自动化测试框架,并深入探讨其设计原则、实现方法及维护策略。通过实际代码示例和清晰的步骤说明,我们将一起探索如何确保软件质量,同时提升开发效率。
25 1
|
10天前
|
敏捷开发 监控 jenkins
探索自动化测试框架在敏捷开发中的应用与优化##
本文深入探讨了自动化测试框架在现代敏捷软件开发流程中的关键作用,分析了其面临的挑战及优化策略。通过对比传统测试方法,阐述了自动化测试如何加速软件迭代周期,提升产品质量,并针对实施过程中的常见问题提出了解决方案。旨在为读者提供一套高效、可扩展的自动化测试实践指南。 ##
29 9
|
4天前
|
监控 测试技术 持续交付
探索自动化测试在软件开发中的最佳实践
本文旨在深入探讨自动化测试在软件开发过程中的应用,以及如何有效地实施自动化测试以提高软件质量和开发效率。通过分析自动化测试的优势、挑战和最佳实践,本文为软件开发团队提供了一套实用的指导方案。
|
9天前
|
安全 前端开发 API
探索后端开发中的API设计原则
【10月更文挑战第41天】在数字化时代的浪潮中,后端开发扮演着至关重要的角色。本文将深入探讨API设计的核心原则,从RESTful API的实现到错误处理的最佳实践,带领读者领略高效、可维护和易于扩展的API设计之美。
|
29天前
|
Web App开发 敏捷开发 存储
自动化测试框架的设计与实现
【10月更文挑战第20天】在软件开发的快节奏时代,自动化测试成为确保产品质量和提升开发效率的关键工具。本文将介绍如何设计并实现一个高效的自动化测试框架,涵盖从需求分析到框架搭建、脚本编写直至维护优化的全过程。通过实例演示,我们将探索如何利用该框架简化测试流程,提高测试覆盖率和准确性。无论你是测试新手还是资深开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧。
|
1月前
|
Web App开发 移动开发 前端开发
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(一)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(一)
46 0
|
1月前
|
Web App开发 前端开发 JavaScript
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(二)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(二)
50 0
|
2月前
|
数据可视化 数据管理 测试技术
聊聊自动化测试框架
关于自动化测试框架的一些理解和思考总结,就是上面这些内容,提到的一些框架组件可能存在不合理的地方,仅供参考,如有更好的建议,请指出,不胜感激
59 4
聊聊自动化测试框架
|
4月前
|
jenkins 测试技术 持续交付
探索自动化测试框架在软件开发中的应用
【7月更文挑战第10天】随着软件行业的快速发展,高效、可靠的软件产品成为企业竞争的核心。自动化测试框架作为提升软件质量与开发效率的关键技术,其在软件开发过程中扮演着越来越重要的角色。本文将深入探讨自动化测试框架的应用,从其定义、优势到具体实施策略,旨在为软件开发团队提供一套完整的自动化测试解决方案。通过实际案例分析,我们将展示如何有效整合自动化测试框架到软件开发生命周期中,以及如何克服实施过程中可能遇到的挑战。