编写-Android-Library-的最佳实践-1,重磅消息

jobject /* this */) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}

class YourClass(private val context: Context) {
init {
System.loadLibrary(your-name-lib")
}
/**

  • A native method that is implemented by the ‘native-lib’ native library,
  • which is packaged with this application.
    */
    external fun stringFromJNI(): String //Kotlin 的 external 关键字 类似 Java 的 native 关键字
    }

尽量包含所有 abi,把选择权交给接入方

在联运 SDK 上线后的一个月,我们收到 cp 反馈接入了之后有奔溃,后来检查发现是 armeabi 下没有 so 文件导致的。这本没有什么问题。但是你没有办法保证接入方应用的 armeabi 文件里也是空的,一旦这里面有 so ,android 就会去这里面找;还有一种可能就是现在很多应用会设置 abiFilter 去过滤掉一些 abi,万一人家只想保留 armeabi,而你的 library 里面又没有,这两种情况都会导致 crash。然而:

ndk r16b 已经弃用armeabi ,r17c 直接移除了对 armeabi 的支持, 如果有生成 armeabi 的需求只能降低 ndk 版本。(感谢评论区 @我啥时候说啦jj整理指出)

所以为了确保兼容,我们必须在 library 的 build.gradle里手动声明自己需要编出哪几个 abi:

defaultConfig {
externalNativeBuild {
cmake {
cppFlags “”
abiFilters ‘arm64-v8a’, ‘armeabi’, ‘armeabi-v7a’, ‘x86’, ‘x86_64’
}
}
}

这么一来你的 library 编出来之后就会包含上面 5 种 abi,确保所有的新老机型起码都不会崩溃,如果你的接入方嫌你的 so 太多太大了,他自己可以在 app编译期间设置过滤,“反正我都有,你自己挑吧”。

Resource 资源

库内部资源的命名不要干扰接入方

相信大家平时开发过程中都有过类似的经历:一旦引入了一些第三方库,自己写代码的时候,想调用某个资源文件,一按提示,IDE 提示的全是这些第三方库里面的资源,而自己 app 里面的资源却要找半天。

我们平时写库的时候难免会自己定义一些 Resource 文件,包括string.xml xxx_layout.xml color.xml 等等,这些库生成的 R.java 一旦参与 app 的编译之后,是可以直接被引用到的,所以自然而言也会被 IDE 索引进提示里面。而照常来讲,一个应用是不应该直接引用一些第三方库里面的资源的,搞不好就很容易出现一些问题。比如万一哪天人家库升级把这串值改掉了,或者干脆拿掉了,你 app 就跪了。

联运 SDK 在开发的时候就注意到了这一点,比如我们的 SDK 叫 MeizuLibrarySdk,那么我在定义 strings.xml时,我会写:

你好
世界

再比如,我需要定义一个颜色,我会在 colors.xml里面写:

#8124F6

相信大家应该已经发现了,每一个资源都会以 mls 开头,这样有个好处,就是别人在引用了你的库之后,用代码提示的时候,只要看到 mls 开头的资源,就知道是你库里面的,不要用。但是这还不够,因为 Android Studio 还是会在人家写代码的时候把你的资源提示出来:

image.png

有没有一种办法,来让 library 开发者可以向 Android Studio 申明自己需要暴露哪些资源,而哪些不希望暴露呢?

当然是有的。我们可以在 library 的 res/values 下面建立一个 public.xml 文件:

这样依赖,如果你在 app 里面试图引用 mls_world,Android Studio 就会警告你引用了一个 private 资源。

这个方法的详细介绍可以看官方文档:

developer.android.com/studio/proj…

但是不知道为什么,这个方法我在15、16年的时候还是有效的。但是升级到 Android Studio 3.3 + Gradle Plugin 3.1.3 之后我发现 IDE 不会再警告了,也可以通过编译,不知道这又是什么坑。但官方文档依旧没有去掉关于这个用法的描述,估计是插件的一个 bug 吧。

第三方依赖库

JCenter() 能引用到的,不要打包进你自己里面

本着“不要重复造轮子”的原则,我们在开发第三方库的时候,自身难免也会依赖一些第三方库。比如用于解析 json 的 Gson,或者用于加载图片的 Picasso。这些库本身都是 jar 文件的,所以之前会有一些第三方库的作者在用到这些库的时候,把对应的 jar 下载到 libs 下面参与编译,最终编译到自己的jar或者aar里面。而接入者的项目原可能已经依赖了这些库,一旦再接入了你的,就会导致错误,提示 duplicated class was found

这种做法与 Gradle 的依赖管理机制完全是背道而驰的。正确的原则应该是:

只要第三方应用自己能从 JCenter/MavenCentral 获取到的库,如果你的库也依赖了,请一概使用 compileOnly

举个例子,比如我的库里面需要发起网络请求,按照 Google 的推荐,目前最好用的库应该是 Retrofit 了,这个时候我应该在 library 的 build.gradle 里这样写:

compileOnly “com.squareup.retrofit2:retrofit:2.4.0”

compileOnly 标明后面的库只会在编译时有效,但不会你 library 的打包。这么一来,你只需要告诉你的引用者,让他们在自己 app 模块的 build.gradle 里加上引用即可,就像这样:

implementation “com.squareup.retrofit2:retrofit:$versions.retrofit”

这样做的好处是,如果引用者的项目本来就已经依赖了 Retrofit,那么皆大欢喜,什么都不用加,并且上面的 $versions.retrofit 意味着引用者可以自己决定他要用哪个版本的 Retrofit,一般来讲只要大于等于你编译库时用的版本都不会有太大问题,除非 Retrofit 自己大量修改了 API 导致编不过的那种。这么一来就再一次把选择权交给了你的引用者,既不用担心冲突,也不用担心版本跟你用的不匹配。

使用单个文件统一依赖库的版本

如果你的项目分了好多模块,结构比较复杂,我这边推荐大家使用一个 versions.gradle 文件来统一所有模块依赖库的版本。这一招并不是我原创的,而是 Google 在 architecture-components 的官方 demo 里体现的。这个 demo 的 Project 包含了大量的 module,有 library 有 app,而所有的 module 都需要统一版本的依赖库,拿 buildToolsVersion 为例,总不能不能你依赖 27.1.1,我依赖 28.0.0 这样。我把链接放在下面,推荐大家都去学习一下这个文件的写法,以及它是如何去统一所有 module 的。

github.com/googlesampl…

API 设计

关于 API 设计,由于大家的库所要实现的功能不一样,所以没有办法具体列举,但是依然在这里为大家分享一些注意点,其实这些注意点只要能站在接入者的角度去考虑,大多数都能想到,而问题就在于你在写库的时候愿不愿意去为你的接入者多考虑一点。

不要在人家的 Application 类里蹦迪

相信暴露一个 init() 方法让你的调用者在 Application 类里做初始化,是很多库作者喜欢干的事。然而大家反过来想一下,我们都看过很多性能优化的文章,通常第一篇都是让大家检查一下自己的 Application 类,有没有做太多耗时的操作?因为 Application 是你应用起来之后第一个要走的,如果你在里面做了耗时操作了,势必会推迟 Activity 的加载,然而这一点却很容易被大家忽略。所以如果你是一个库的作者,请:

  1. 不要在你的 init() 方法里做任何耗时操作
  2. 更不要提供一个 init() 方法,让人家放在 Application 类里,还让人家“最好建议异步”,这跟耍流氓没区别

统一入口,用一个平台类去包含所有的功能

这里的平台类是我自己取的名字,你可以叫 XXXManagerXXXProxyXXXServiceXXXPlatform都可以,把它设计成单例,或者把内部所有的方法写成静态方法。不要让你的调用者费劲心思去找应该实例化哪个类,反正所有的方法都在这一个类里面,拿到实例之后调用对应的方法即可。这样统一入口,既降低了维护成本,你的调用者也会感谢你。

所有的常量,定义到一个类

if (code == 10012) {
//do something
}

这个 10012 是什么?是你库里面定义的返回码?那为啥不写成常量暴露给你的调用者呢?

@Keep
class DemoResult private constructor(){

@Keep
companion object {
/**

  • 支付失败,原因:无法连接网络,请检查网络设置
    */
    const val CODE_ERROR_CONFIG_ERROR: Int = 10012
    const val MSG_ERROR_CONFIG_ERROR: String = “配置错误,请检查参数”


}
}

这样一写,你的调用者只要点点鼠标,进来看一下你这个类,就能迅速把错误码跟错误提示对应上。懒一点的话,他们甚至可以直接用你定义的这些提示去展现给用户。而且万一有一天,服务端的同事告诉你,10012 需要变成别的值,此时你只需要修改你自己的代码就行,对库的接入者而言,它依然是 DemoResult.CODE_ERROR_CONFIG_ERROR ,不需要做任何修改,这样方便接入者的事何乐而不为呢?

帮助接入者检查传入参数的合法性

如果你的 API 对传入的参数有要求。建议在方法执行的第一步就对参数予以检查。一旦调用者传递的参数不合法,直接抛异常。有很多开发者觉得抛异常这种行为不能接受,因为毕竟这在 Android 平台的直接表现就是 app crash。但是于其让 app 在用户手里 crash,还不如直接在开发阶段 crash 掉让开发者立刻注意到并且予以修复。

这里以 String 的判空为例,如果你用 Kotlin 来开发,一切都简单多了。比如我现在有一个实体如下:

data class StudentInfo(val name: String)

一个 StudentInfo 是必须要有一个 name 的,并且我声明了 name 是不为空的。这个时候如果你在 Kotlin 里面实例化 Student 并且 name 传空,是直接编译不过的。而对于 Java 而言,Kotlin 帮我们生成的 class 文件也已经做好了这一点:

public StudentInfo(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, “name”);
super();
this.name = var1;
}

继续看 checkParameterIsNotNull() 方法:

public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
throwParameterIsNullException(paramName);
}
}

throwParameterIsNullException()就是一个比较简单的抛异常了。

private static void throwParameterIsNullException(String paramName) {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

// #0 Thread.getStackTrace()
// #1 Intrinsics.throwParameterIsNullException
// #2 Intrinsics.checkParameterIsNotNull
// #3 our caller
StackTraceElement caller = stackTraceElements[3];
String className = caller.getClassName();
String methodName = caller.getMethodName();

IllegalArgumentException exception =
new IllegalArgumentException("Parameter specified as non-null is null: " +
"method " + className + “.” + methodName +
", parameter " + paramName);
throw sanitizeStackTrace(exception);
}

所以即便你用的是 Java, 试图直接 Student student = new Student(null),运行时也是会直接 crash 掉并且告诉你 name 不能为空的。联运 SDK 有大量的参数检查用了 Kotlin 的这一特性,使得我少些了很多代码,编译器编译后会自动帮我生成。

这里要推荐大家参考一下 android.support.v4.util.Preconditions ,这个里面封装好了大量的数据类型的情景检查,源码一看就明白。希望大家在写一个库的时候,都能做好传入参数合法性的检查工作,把问题发现在开发阶段,也能确保运行阶段不被意外值搞到奔溃。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

题外话

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊~

这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,点击这里免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

最后如果马化腾把腾讯给你一天,你会来做什么?欢迎评论区讨论。

这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,点击这里免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

[外链图片转存中…(img-nfS7ewyh-1711100431587)]

最后如果马化腾把腾讯给你一天,你会来做什么?欢迎评论区讨论。

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值