技术碎周报第 1 期 (2022.08.30)

1,729

最近想着如何把一些小的技术知识和细节整理起来。参考别人的博客,我给这类文章起了一个名字叫“技术碎周报”。主要用来整理和分享日常开发中遇到的小的知识点和感悟。对于能够独立写成一篇文章的技术总结,我还是按照老的方式以一篇独立的文章的形式整理处理。

1、三个提升代码质量的技巧

首先分享几个日常开发过程中总结的能够提升代码质量的技巧。

1.1 利用条件判断的截断效应

所谓的截断效应就是在 or/and 判断条件(也就是 Java 代码中的 ||&&)判断的时候,如果前面的一部分能够决定这个表达式的结果,后面的条件就不会执行了。比如,

fun sample(node: Node)
    // 判断条件 1
    if (node == null || node.name == 'sample') {

    }

    // 判断条件 2
    if (node != null && node.name == 'sample') {

    }
}

|| 条件中,如果前面的 node == null 为 true,那么后面的逻辑就不会执行了。在 && 条件中,如果前面的 node != null 为 false,那么后面的逻辑就不会执行了。

我们可以充分利用条件判断的这个特性,把更容易出错的或者性能比较差的条件放在后面,这样只要前面的能够决定整个表达式的结果,后面的条件就不执行了。因此,可以降低出错的概率,提升程序的性能。

1.2 有 if 必有 else

当你还是一个代码的新手的时候,一个降低代码逻辑错误的思维方式就是 有 if 必有 else. 这可以很大程度上降低自己漏掉某些判断条件的概率。即便有些情况不需要处理,增加一行编译开发和分析问题的日志对提高自己的编码效率也是有帮助的。比如,

fun unregisterReceiver(receiver: BroadcastReceiver) {
    when (processes[receiver]) {
        true -> {
            synchronized(lock) {
                val action = actions.remove(receiver)
                if (action != null) {
                    val globalReceiver = globals[action]
                    if (globalReceiver != null) {
                        receivers[globalReceiver]?.remove(receiver)
                        L.i("Receiver [$receiver] unregistered!")
                        // 如果所有子广播已经全部取消,取消全局广播监听
                        if (receivers[globalReceiver]?.isEmpty() == true) {
                            globals.remove(action)
                            receivers.remove(globalReceiver)
                            UtilsApp.getApp().unregisterReceiver(globalReceiver)
                        }
                    } else {
                        L.e("Failed to unregister receiver [$receiver]: " +
                                "global receiver not found! WARN: This might lead to memory leak!")
                    }
                } else {
                    L.e("Failed to unregister receiver [$receiver]: " +
                            "action not found! WARN: This might lead to memory leak!")
                }
            }
        }
        false -> {
            LocalBroadcastManager.getInstance(UtilsApp.getApp())
                .unregisterReceiver(receiver)
        }
        else -> {
            L.e("Failed to unregister receiver[$receiver]: " +
                    "process not found! WARN: This might lead to memory leak!")
        }
    }
    processes.remove(receiver)
}

1.3 合理利用命名规则

一个好的命名习惯可以增加自己代码的可读性并降低出错的概率。所以,一般的大厂对变量、方法、类和包名的定义都有自己的规范。

这里我举一个在 Android 中的例子。比如,我经常看到一些代码在定义一个控件的时候使用诸如 image 这样的名字。这类名字的缺点是从命名上,你看不出它具体是一个控件还是一个资源图片等。

如果我们使用控件的缩写作为变量的前缀,比如比如 ImageView 类型的控件,命名的时候可以用 iv 开头;TextView 类型的控件命名的时候以 tv 开头。这样如果上述 image 是一个控件,那么它的命名应该是 ivImage。这样,通过命名我们就可以判断出这里的 image 可能是一个 Bitmap 或者图片的 url 等而不是一个控件。

2、开源应用源码片段三则

这里主要是从开源软件的源码中抠出一些有价值的代码。

2.1 调用系统控件打印 PDF 逻辑

调用系统 API 打印 PDF,需要基于 WebView 执行。虽然,我在自己的项目中早就用过类似的功能。不过,需要注意的是当在后台执行打印操作的时候,我们需要像下面这样,定义一个 WebView,然后需要注意当页面加载完毕,也就是 onPageFinished 被调用的时候再执行后续的调用系统 API 的操作,

fun create() {
    val webView = WebView(context)
    webView.loadDataWithBaseURL(baseURL, content, mimeType, encoding, null)
    webView.webViewClient = object : WebViewClient() {

        override fun onPageFinished(view: WebView?, url: String?) {
            val printDocumentAdapter = webView.createPrintDocumentAdapter(file.nameWithoutExtension)
            generatePDF(printDocumentAdapter)
        }
    }
}

private fun generatePDF(printDocumentAdapter: PrintDocumentAdapter) {
    val postPDFPrinter = PostPDFPrinter(file, printDocumentAdapter, printAttributes, onResult)
    postPDFPrinter.print()
}

源码地址:

https://github.com/CostCost/Notally/blob/master/Post/src/main/java/com/omgodse/post/PostPDFGenerator.kt

2.2 获取顶部的 Activity 信息

通常开发的时候我们一般用 AS 间接使用 ADB. 但实际上 ADB 有很多功能。我在之前的文章中也介绍过 ADB 用来做自动化点击玩游戏的做法。这里介绍的一个 ADB 应用是通过 ADB 获取顶部的 Activity 信息,

adb shell dumpsys activity top

该指令会输出一堆 Activity 信息,可能输出多个 Activity 的信息,最顶部的排在最后。此外,还会输出 Activity 的布局信息,Fragment 信息等。不过,感觉输出的格式可能并不固定,比如有些排版就有问题,所以可能需要做多个版本适配。此外,非要追根到底的话,应该查看 ADB 的实现原理,自己写一份与设备通信。

ADB 的有些功能现在在高版本的设备上面受限了,有些三方应用可以在自己的应用内获取其他应用的布局信息。不知道是不是也是用了上面这个原理。

如果需要了解如何在 Android 内执行 ADB 指令的话,可以参考下面的代码,

https://github.com/Shouheng88/AndroidUtils/blob/master/utils/src/main/java/me/shouheng/utils/device/ShellUtils.java

源码地址:

https://github.com/CostCost/AppActivityName/blob/master/src/com/zgh/util/Main.java

2.3 在任务栏里隐藏 Activity

接下来的两个和下面的这个开源软件有关。源码地址

https://github.com/zhanghai/TextSelectionWebSearch

这是挺有趣的软件,它的功能是在长按某个文本之后出现一个自定义的搜索选择,点击之后跳转到我们指定的浏览器对文本进行搜索。这里涉及到两个知识点。其中一个是在系统的任务栏里面隐藏自己的 Activity.

可以通过为 Activity 增加 excludeFromRecents 属性实现该 Activity 不展示到系统的任务栏。该属性并不会仅仅影响被设置的 Activity. 由此该 Activity 启动的后续同属一个堆栈的一系列 Activity 都不会出现在“最近打开”的任务栏。也就是说该属性是对 Task 起作用的,而不仅仅是某个 Activity.

另一个是需要设置 android:noHistory="true"。设置该属性后,该 Activity 在堆栈中不留历史痕迹。默认的值是 false. 举例说明,假设有三个 Activity 分别是:A,B,C. 这三个 Activity 可以依次顺序启动下一个Activity. 次日如如果在 AndroidManifest.xml 中配置 B 的属性为:android:noHistory="true"。其他两个不做特别设置,仅仅作为一般的 Activity 处理。可以观察到,A 启动后,从 A 跳转到 B,再从 B 跳转到 C。进入 C 后,此时如果按返回键,将直接进入 A,而不是 B。综上,可以这么理解 android:noHistory="true" 对 Activity 行为的影响:当该 Activity 屏幕不可见时,相当于 Android 系统调用 Activity 的 finish() 方法结束了该Activity。

源码地址:

https://github.com/CostCost/TextSelectionWebSearch/blob/master/app/src/main/AndroidManifest.xml

该开源软件的另外一个知识点下期和另一个类似功能的知识点一起总结 :)

3、Kotlin 使用心得和踩坑总结两则

3.1 拓展方法的使用心得:能在类中添加方法时就不要使用拓展方法

我有时候在 review 别人的代码的时候看到,有的同学很喜欢使用 Kotlin 的拓展方法和拓展字段的特性。比如,

val CountdownRingDrawable.isCountingDown: Boolean
    get() = currentRemain > 0

这没什么问题,但是当我们拥有这个类的权限,可以对它直接进行修改,并且新增的特性和方法是通用的的时候,不建议使用拓展字段和拓展方法。主要原因是拓展的字段和拓展仍然游离于类本身之外,不便于该类相关的方法的统一收拢。如果只针对自己的需求新增一个拓展方法或属性,可以考虑使用 Kotlin 的特性。

3.2 缺省函数处理机制引发的异常

如下两个方法,方法 2 是在方法 1 的基础上为了兼容老的方法新增的一个方法,

// 方法 1
fun xxxx(param1: String?): String {
    // ...
}

// 方法 2
fun xxxx(param1: String?, param2: Boolean = false, param3: String = ""): String {
}

如果所有模块都是源码编译则不存在任何问题。但是如果调用以上方法的某个模块以编译为 jar 则会出现方法找不到的异常。这是因为实际上 Kotlin 在处理缺省参数函数的时候会新增一个静态方法,并在该方法内部通过参数判断的方式,当某个参数没传的时候就使用默认值,

public final String xxxx(@Nullable String param1, boolean param2, @NotNull String param3) {
    Intrinsics.checkNotNullParameter(param3, "param3");
    return "";
}

// $FF: synthetic method
public static String xxxx$default(KotlinDefaultParameterTest var0, String var1, boolean var2, String var3, int var4, Object var5) {
    if ((var4 & 2) != 0) {
        var2 = false;
    }

    if ((var4 & 4) != 0) {
        var3 = "";
    }

    return var0.xxxx(var1, var2, var3);
}

解决这个问题的一个方法是新增一个重载方法。另一个解决办法是为上述方法增加 @JvmOverloads 注解。此时的反编译结果如下。也就是当加了这个注解之后,Kotlin 会根据原来的方法新增一系列重载方法。不过后面这种方式的一个缺点是,可能会导致类的方法量暴增。

@JvmOverloads
@NotNull
public final String xxxx(@Nullable String param1, boolean param2, @NotNull String param3) {
    Intrinsics.checkNotNullParameter(param3, "param3");
    return "";
}

// $FF: synthetic method
public static String xxxx$default(KotlinDefaultParameterTest var0, String var1, boolean var2, String var3, int var4, Object var5) {
    if ((var4 & 2) != 0) {
        var2 = false;
    }

    if ((var4 & 4) != 0) {
        var3 = "";
    }

    return var0.xxxx(var1, var2, var3);
}

@JvmOverloads
@NotNull
public final String xxxx(@Nullable String param1, boolean param2) {
    return xxxx$default(this, param1, param2, (String)null, 4, (Object)null);
}

@JvmOverloads
@NotNull
public final String xxxx(@Nullable String param1) {
    return xxxx$default(this, param1, false, (String)null, 6, (Object)null);
}

4、介绍我的技术博客

怎么说我也是浸淫在互联网行业多年,总结和整理了大量的文章。因为微信公众号每天发布文章上限问题,以及有些文章过于久远,估计发出来很多读者也没什么兴趣,所以,我把这些文章都放到了技术博客上面。其实这个博客做成有一段时间了,只是一直没来得及介绍,

QQ截图20220829222544.png

如图所示,目前已经发布的文章大概有 93 篇,包含许多初级和高级的文章。如果需要的话可以到网站来看看~

地址是,

https://www.fullstack.fan

以上是第一期的内容,如果觉得好的话,就来关注我哦 ♥