@shark0017
2019-01-07T14:38:02.000000Z
字数 30302
阅读 2876
书籍
目前技术圈的人或多或少都开发过库项目,无论是因为要靠它来找工作,还是准备通过其进行学习交流,亦或是借此来招摇撞骗,毋庸置疑的是开发第三方库这件事已经变得越来越流行了。
技术圈本不应该被娱乐化的,浮躁和喧嚣对技术来说是致命的。以经验来讲,解决issue才能促使开发者更快的学习,仅仅炫耀自己的star数是毫无意义的。
一个好的库作者应该具备以下能力:
Sqlite的作者们本身已经将代码完全公开了,这几个作者还是十年如一日的提交代码,这种严谨性和能力都是值得我们学习的。
图中提交历史来自:https://sqlite.org/docsrc/timeline
做任何事情之前都应该了解基本法,有法才能守法。开源协议或者软件许可证就是开源世界中的法令,作为库的开发者都应该了解一下。
维基百科对于license的定义如下:
软件许可证是一种具有法律性质的合同或指导,目的在规范受著作权保护的软件的使用或散布行为。通常的授权方式会允许用户来使用单一或多份该软件的复制,因为若无授权而径予使用该软件,将违反著作权法给予该软件开发者的专属保护。效用上来说,软件授权是软件开发者与其用户之间的一份合约,用来保证在匹配授权范围的情况下,用户将不会受到控告。
面对众多的协议,如何选择协议就是一个问题了,下面是完备的选择协议流程图:
开源许可有上百种(Various Licenses and Comments about Them),好在我们只需要了解最流行的六种就好,即GPL、BSD、MIT、Mozilla、Apache和LGPL。
一般情况下,我们开发的开源库是无条件分析给使用者的,而使用者大多都是盈利性质的闭源app开发者,所以我们着重了解下BSD、Apache和MIT协议。
BSD开源协议是一个给使用者很大自由的协议,它允许使用者修改原有代码,并且允许使用者进行闭源和发布,属于最为商业友好的开源协议之一。很多商业公司引入开源库的选型上都会首选BSD的库,这样可以免于以后的各种纠纷。
虽然我们可以享受到BSD给我们的各种权利,但不要忘记我们的责任和义务:
Apache Licence是著名的非盈利开源组织apache定义的一套协议,有多个版本。该协议和BSD类似,也是商业友好的协议。
使用者需要满足的条件也和BSD类似:
我们使用的android代码中就经常可见此类的声明信息:
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.app;
import android.content.Intent;
import android.content.res.Configuration;
// ...
我们知道Linux内核的许可证是自由软件基金会的GPL(v2),但是android的主要代码是apache许可证(ASL)。GPL规定所有对源码的修改和衍生都是必须公开的,并且要求使用者以相似的许可证发布,但google的android却不适GPL的,关于这里的故事可以去网上搜索一下。
Google从收购android以来一直使用ASL,这个协议在android初期帮助google打消了合作者们的一些顾虑,吸引了大量的OEM合作伙伴,这也使得android的活跃用户数量在现今达到了惊人的16亿。
Google在近期推出了一个专利许可计划——PAX。PAX是google最新推动的一项创新性的专利许可计划,旨在提供更宽松和平的专利解决方案。PAX面向所有人免费开放,成员会在免专利费的基础上彼此许可,许可范围涵盖合格设备上安装的android和google应用程序。也就是说google组建了一个专利联盟,这个联盟内的成员可以友好的使用联盟专利库中的专利,共同创造优秀的软件而免于陷入专利纠纷。
题外话:
Google在android上的做法也是一个花招,它遵循了GPL,做了开源。但google用了linux kernel,修改了内核的代码,因为kernel使用的是ASL,所以google对于使用的硬件驱动都保持了闭源,仅仅对android代码做了开源。
MIT协议又称麻省理工学院许可证,最初由麻省理工学院开发和制定。MIT是和BSD一样是宽范的许可协议,作者只想保留版权,而无任何其他的限制。但你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码形式进行发布的。
被授权人权利:
被授权人义务
Facebook在原先的react和react-native项目中采用的是BSD+PATENTS License,这个组合协议看似没有任何问题,但是在附带的PATENTS专利许可中有了如下的说明:
The license granted hereunder will terminate, automatically and without notice, if you (or any of your subsidiaries, corporate affiliates or agents) initiate directly or indirectly, or take a direct financial interest in, any Patent Assertion: (i) against Facebook or any of its subsidiaries or corporate affiliates, (ii) against any party if such Patent Assertion arises in whole or in part from any software, technology, product or service of Facebook or any of its subsidiaries or corporate affiliates, or (iii) against any party relating to the Software
简单来说就是如果你的项目用到了react的代码,你又和facebook产生了商业上的瓜葛,那么你的许可会被直接撤销。当你和facebook对诸公堂的时候,你的项目很可能会变为无授权的状态。
这一条款引起了很多公司的反感,很多大型的公司甚至开始放弃react。在社区中,很多开发者也认为这是一种破坏开发生态的行为,对facebook表示了强烈的谴责。
在重压之下,facebook的开发者公布了最新的方案,将react和react native项目改为MIT许可。
这一举动看似是几个单词的修改,但是确实影响了整个前端世界。如果你翻阅过react的license,很少会看到有这么多次commit的license文件,也很少会看到有这么多comment的license文件。
详细内容:Change license and remove references to PATENTS
如果你不清楚该如何选择自己的开源协议,那么可以去“Choose a License网站”来快速的选择自己的协议并复制下来,这是笔者可以找到的最简单的方案了。
现在我们已经定好了协议,准备要开始写代码了。但你先别急,当你在想要实现一个库之前,请用一个小时的时间去分析自己想法的可行性,然后去github上搜索一下有没有类似的库,或者是通过群组来询问一下相关的信息。如果你搜索到github上有个和你想做的库类似的东西,你完全可以了解其原理后拿来就用,这会节约你很多的时间。
我们必须知道,当提出一个想法的时候,别人很可能也已经想过了,而差别就在于别人是否已经实现并开源了。
有句话是这么说的:当你想要做一件事情的时候,它已经迟了
当然了,很多时候你可能都没有好的运气,找到一个称心如意的库实属不易。当我们搜索到了一个和自己想法类似的库后,最好的做法是通过issue联系到作者,提出问题和思想的差异点。如果可能的话,可以给出自己的解决方案,进行探讨交流。开发者的时间都很宝贵,为何不花时间来维护同一个东西,干嘛不努力让它变成精品呢?
在大学中的时候android 5.0刚刚兴起,在eclipse时代我就和一个作者共同维护了一个material的效果库,这个过程中我学到了很多知识,也为我之后开源selectorInjection做了铺垫。
什么时候可以重复造轮子?
Github上有很多很多作者,那么自然就产生了社会性。很多提交和留言都可能被作者无视,或者作者早就转行了。遇到这样的情况,我的做法是fork代码,然后自己开始维护。
DebugDrawer就是一个例子,他原作者的更新动力不足,而且代码冗余较高,不得已的情况下我只能自己维护了。比如像google就是fork了square/dagger的仓库,自己进行了二次开发,也取得了很大的成功。
题外话:
接着维护一个项目是完全合理的,但一定要fork之前的仓库。要时刻记住,我们都是踩在前人的肩膀上,不要狂妄,要保持谦虚。
如果你的库是给别人使用的,那么请在设计api的时候多花点时间。因为,一旦有人用了你的库,就有了历史负担,如果你后期随意地改变方法名和参数,使用者很有可能会因此而不再更新,甚至抛弃这个库。
有很多开发者忽略了包名的设计,包名其实也是api的一部分,必须保持稳定。对于名下有众多开源库的开发者,可以参考JakeWharton的做法值得一看。他将项目名称作为包名,抛弃以com.*
开头的传统写法,保证所有的库都不会有包名冲突。当然了,你也可以自行选择合适的,易于管理的包名命名策略。
既然说到了接口,那么java接口的传参也是接口的一个设计点,个人的经验是把内部类区块写到参数靠后的位置,把context放在参数的前部,如果是接口则加上I
的前缀,如果是抽象类则用Abs
的前缀:
public interface Test{
// context在前,内部类在后
void test(Context context, IModule module, OnClickListener listener);
}
如果后期要更改名字和废弃方法,可以用@Deprecated
来做标记,把要变动的东西先标记为废弃,过了几个版本后再删除掉,这也是google当时删除httpClient时的做法。
题外话:
虽然我们有@Deprecated来,但是如果你的库使用者众多,那么建议多维护几个版本后再删除废弃的方法,反例可以参考google删除httpClient后的动荡。
对于一些上层库,使用者可能想有自己的实现,这时候可以考虑用delegate的方式来做。
FragmentActivity中将权限的处理交给了permissionCompatDelegate来做,一方面是简化activity的复杂度,一方面能通过组合的形式实现此功能,方便兼容多个android版本。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mFragments.noteStateNotSaved();
int requestIndex = requestCode>>16;
ActivityCompat.PermissionCompatDelegate delegate =
ActivityCompat.getPermissionCompatDelegate();
if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) {
// Delegate has handled the activity result
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
如果我们想要允许使用者替换delegate,那么可以参考源码中resources类的做法,建立对应的impl对象:
public class Resources {
/**
* Set the underlying implementation (containing all the resources and caches)
* and updates all Theme references to new implementations as well.
* @hide
*/
public void setImpl(ResourcesImpl impl) {
if (impl == mResourcesImpl) {
return;
}
mResourcesImpl = impl;
// Create new ThemeImpls that are identical to the ones we have.
synchronized (mThemeRefs) {
final int count = mThemeRefs.size();
for (int i = 0; i < count; i++) {
WeakReference<Theme> weakThemeRef = mThemeRefs.get(i);
Theme theme = weakThemeRef != null ? weakThemeRef.get() : null;
if (theme != null) {
theme.setImpl(mResourcesImpl.newThemeImpl(theme.getKey()));
}
}
}
}
}
这种优良的设计使得后续android低版本兼容svg成为了可能。因为资源是通过resource来加载的,早期的版本加载图片不支持svg,那么在support库中只要实现一个低版本兼容的resourcesImpl就可以了。
源码中加载drawable的实现:
@NonNull
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
我们在设计sdk的时候要尽可能的保证规范的编码风格,将代码灵活性放在首位,给使用者带来更多的方便。
有时候我们希望有些方法和变量仅仅在库中给库作者自行调用,不希望使用者调用到这类方法。最常见的一种方式是将相互调用的代码放在同一个包中,用protected来做修饰,这是大多数库的策略。那么如果这个方法必须为public,但我们还想要进行限制呢?
有如下几个方案:
示例:
/**
* 外部不要去直接调用
*
* @hide 私有变量
*/
@RestrictTo(RestrictTo.Scope.LIBRARY) // 表示作用域仅仅在当前库中,外部使用会有警告
public static ChapterHelper _chapterHelper;
Scope的取值有很多,可以通过文档和源码了解一下:
@Retention(CLASS)
@Target({ANNOTATION_TYPE,TYPE,METHOD,CONSTRUCTOR,FIELD,PACKAGE})
public @interface RestrictTo {
/**
* The scope to which usage should be restricted.
*/
Scope[] value();
enum Scope {
/**
* Restrict usage to code within the same library (e.g. the same
* gradle group ID and artifact ID).
*/
LIBRARY,
/**
* Restrict usage to code within the same group of libraries.
* This corresponds to the gradle group ID.
*/
LIBRARY_GROUP,
/**
* Restrict usage to tests.
*/
TESTS,
/**
* Restrict usage to subclasses of the enclosing class.
* <p>
* <strong>Note:</strong> This scope should not be used to annotate
* packages.
*/
SUBCLASSES,
}
}
通过注解代替枚举可以减少内存开销,在android studio越来越智能的情况下,其编码方式和枚举几乎一致,也支持代码提示。以tianzhijiexian/shareLoginLib为例,在编码的时候会用注解来表示分享到第三方的内容类型,比如给微信分享图文信息。
@Retention(RetentionPolicy.SOURCE)
@IntDef({ContentType.TEXT, ContentType.PIC, ContentType.WEBPAGE, ContentType.MUSIC})
public @interface ContentType {
int TEXT = 1, PIC = 2, WEBPAGE = 3, MUSIC = 4;
}
如果你需要将注解暴露给使用者,那么推荐采用string的形式
来做,因为string的值有很高的可读性。在android studio目前还没智能到能识别变量的情况下,强烈建议在对外接口中用string来代替枚举。
@Retention(RetentionPolicy.SOURCE)
@StringDef({LoginType.WEIXIN, LoginType.WEIBO, LoginType.QQ})
public @interface LoginType {
String WEIXIN = "WEIXIN", WEIBO = "WEIBO", QQ = "QQ";
}
如果你做的是第三方sdk,那么很可能会需要很多权限。对于这部分的方法调用,可以用@RequiresPermission注解进行标注。如果要检查有效权限列表中是否存在某个权限,可使用anyOf()。
比如,设置手机壁纸之前应检查权限:
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;
比如,我们要从外部存储区域copy文件到内部存储区域,那么则需要两个权限:
@RequiresPermission(allOf = { // all of表示需要所有的权限
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE})
public static final void copyFile(String dest, String source) {
// ...
}
开发第三方库的时候对于这种代码约定注解会用的比较多,公司项目中使用的到不多。之前给twitter提交代码的时候,如果不用注解还会被驳回。Android官网上给出了很各种各样的注解,但我们只需要记得常用的就好,没必要为了用注解而用,要知道我们的目的仅仅是给使用者一些提示而已。
注解仅仅是辅助,如果你代码写的十分清楚,有很高的可读性,那完全可以不用注解。
官方文档:https://developer.android.com/studio/write/annotations?hl=zh-cn
当我们做一个注解库的时候需要了解如何定义注解的类型。
/**
* Annotation retention policy. The constants of this enumerated type
* describe the various policies for retaining annotations. They are used
* in conjunction with the {@link Retention} meta-annotation type to specify
* how long annotations are to be retained.
*
* @author Joshua Bloch
* @since 1.5
*/
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
三个标识的意义:
三个标识的使用场景:
-keepclassmembernames @interface * { <methods>; } // 请用自己的注解名补全这条规则
令人好奇的是,为什么一个在编译时使用的注解还需要不被混淆呢?
ProGuard工具在混淆时针对的不是java文件,而是class文件,proguard是对.class字节码的混淆。ProGuard的混淆策略很可能将“多方法注解类”中的方法混淆为多个同名方法,而同名方法在编译时是不合法的。
举例:
@Retention(CLASS)
public @interface MyClass {
String getName() default "kale"; // 方法01
boolean getName1() default false; // 方法02
}
这个注解里面有两个方法,当我们开启了proGuard的-overloadaggressively功能,它在混淆的时候可能会混淆出如下代码(仅仅是示例):
@Retention(CLASS)
public @interface a {
String a();
boolean a();
}
我们知道同参数不同返回值的方法在ide中是非法的,但在字节码中却是合法的。字节码中并不会出现因为两个方法同名,但返回值不同就报错的问题。也就是说如果你在记事本中写好类似的方法,手动进行javac编译,那么类似的代码是完全可以运行的。
下面就有几个知识点:
也就是说如果你没有keep掉这个注解,那么当你开启混淆的时候,在上述条件成立的情况下就会出现打包失败的问题。所以说,如果注解仅仅用于编码时,那么用SOURCE做标识最合适,因为它不进入.class文件。
第三方库的资源会和使用者的项目进行合并,如果你的资源和使用者的资源都取了同一个名称,那么就会进行覆盖,产生很多难以察觉的冲突,所以最好增加自定义的前缀。
以tianzhijiexian/debugDrawer为例,这个库的layout资源前面都会加特殊的前缀dd(debugDrawer -> dd):
除了layout文件,color等资源的命名也应该注意的。库作者多注意这些细节点后,会给使用者省去很多麻烦,减少不必要的冲突。
如果你的库用到了assets下的文件,那么在assets文件夹中也应该建立一个子目录,通过目录名称来防止覆盖冲突。
Android-Debug-Database直接在assets中放文件的做法则是一个反例:
在制作第三方登录、分享库时,需要在manifest中定义一些key(用qq分享时)。但是我们自然不希望将这些值写死,而是交由使用者进行填写,可以用gradle将其变量化:
变量化后,使用者只需要在gradle中进行赋值即可:
android {
defaultConfig {
applicationId "com.kale.share.demo"
manifestPlaceholders = [
// 这里需要换成:tencent+你的AppId
"tencentAuthId": "tencent123456",
]
}
}
值得注意的是,${applicationId}
是一个默认的变量,其值随实际项目的包名而定。在manifest中指定具体包名的时候可以用一下这个默认变量:
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.kale.demo.app" // 请务必写成app的包名
minSdkVersion 18
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {
abiFilters "armeabi"
}
}
}
以自身的经验,一个小型库的方法数不应超过300,我们需要时刻留意自己是否在做一个单一功能的库。这个300不是权威指标,只是希望库开发者尽可能让库代码轻量干净,减少使用者引入库的负担。
一个第三方库的“方法数”和“文件大小”都是使用者会考虑的点,推荐使用methodsCount来进行库方法数目的检测:
我们还可以通过它提供的图表来量化自己库的数据,下面是shareLoginLib的走势图:
(左侧为方法数曲线图,右侧为包大小曲线图)
很多图片选择库、第三方分享库都强制使用者实现onActivityResult()。而onActivityResult()本身就是一个很难维护的设计,在大型项目中onActivityResult里经常会有多个switch,十分难以维护。作为第三方库的作者应该提供更加简单、优雅的回调,避免这种增加使用者项目复杂度的做法。
我们的目标是提供简单的异步回调,比如像下面的代码:
Intent intent = new Intent(this, SecondActivity.class);
request.startForResult(intent, new ActResultRequest.Callback() {
public void onActivityResult(int resultCode, Intent data) {
Toast.makeText(MainActivity.this, "" + resultCode, Toast.LENGTH_SHORT).show();
}
});
至于实现方案,推荐用fragment来做onActivityResult事件的分发处理,下面是demo:
public class OnActResultEventDispatcherFragment extends Fragment {
public static final String TAG = "on_act_result_event_dispatcher";
private SparseArray<ActResultRequest.Callback> mCallbacks = new SparseArray<>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
public void startForResult(Intent intent, ActResultRequest.Callback callback) {
mCallbacks.put(callback.hashCode(), callback);
startActivityForResult(intent, callback.hashCode());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
ActResultRequest.Callback callback = mCallbacks.get(requestCode);
mCallbacks.remove(requestCode);
if (callback != null) {
callback.onActivityResult(resultCode, data);
}
}
}
public class ActResultRequest {
private OnActResultEventDispatcherFragment fragment;
public ActResultRequest(Activity activity) {
fragment = getEventDispatchFragment(activity);
}
private OnActResultEventDispatcherFragment getEventDispatchFragment(Activity activity) {
final FragmentManager fragmentManager = activity.getFragmentManager();
OnActResultEventDispatcherFragment fragment = findEventDispatchFragment(fragmentManager);
if (fragment == null) {
fragment = new OnActResultEventDispatcherFragment();
fragmentManager
.beginTransaction()
.add(fragment, OnActResultEventDispatcherFragment.TAG)
.commitAllowingStateLoss();
fragmentManager.executePendingTransactions();
}
return fragment;
}
private OnActResultEventDispatcherFragment findEventDispatchFragment(FragmentManager manager) {
return (OnActResultEventDispatcherFragment) manager.findFragmentByTag(OnActResultEventDispatcherFragment.TAG);
}
public void startForResult(Intent intent, Callback callback) {
fragment.startForResult(intent, callback);
}
public interface Callback {
void onActivityResult(int resultCode, Intent data);
}
}
题外话:
上述代码仅仅是简单的例子,并没有做复杂的处理。例子中没有提供自定义requestCode的方案,也没有提供rxJava的回调,可以由大家自行修改。
有很多库是需要进行混淆配置的,但让使用者去配置混淆文件总是不太友好。consumerProguardFiles
的出现可以让库作者在库中定义混淆参数,将混淆配置对使用者进行屏蔽。
ShareLoginLib这个库中的例子:
apply plugin: 'com.android.library'
android {
compileSdkVersion 24
buildToolsVersion '24.0.2'
defaultConfig {
minSdkVersion 9
targetSdkVersion 24
consumerProguardFiles 'consumer-proguard-rules.pro' // 库中自定义的混淆配置
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
著名的数据库realm也用到了这样的配置:
base {
dimension 'api'
externalNativeBuild {
cmake {
arguments "-DREALM_FLAVOR=base"
}
}
consumerProguardFiles 'proguard-rules-consumer-common.pro', 'proguard-rules-consumer-base.pro'
proguardFiles 'proguard-rules-build-common.pro'
}
打包工具会将*.pro
文件打包进aar中,app在混淆时候会自动使用此混淆配置来做库代码的混淆,使用者完全无需操心。
以consumerProguardFiles
方式加入的混淆文件具有以下特性:
如果你对于consumerProguardFiles有疑问,可以去stkent/consumerProGuardFilesTest这个项目了解更多的信息。
如果我们开源的是自定义view库,那么肯定会有自定义的view属性。对于这些属性,我们需要遵守严格的规范才可以让编辑器支持代码提示。
一个良好的代码提示效果:
为了支持这样的效果,我们需要在自定义view的属性时用含有这个属性的view的名字做属性名,我们来看个例子。
比方说我们这个view叫做SelectorRadioButton
,那么它的属性集合的名称也必须叫这个:
如果我们用到的属性是android自身已经提供的,或是其他地方已经定义过的,那么在定义属性的时候就不要定义这个属性的类型了,仅仅定义属性名称即可(也就是不要使用format)。
没有定义过的属性:
<declare-styleable name="MyView">
<attr name="viewColor" format="color" /> // 要使用format来定义类型
<attr name="viewWidth" format="dimension" />
</declare-styleable>
利用android中已经提供的属性:
<attr name="android:drawableLeft" />
<attr name="android:drawableRight" />
<attr name="android:drawableTop" />
<attr name="android:drawableBottom" />
其他地方已经定义过的属性:
<attr name="normalColor" />
<attr name="pressedColor"/>
<attr name="checkedColor"/>
<attr name="disableColor"/>
作为库的生产者,强烈不建议引入再引入其他的库。友盟推送的代码就是一个典型的反例,作为一个推送库却引入了okhttp、okio等其他库,臃肿不堪,完全没有让人使用的欲望。
一个第三方库引入其他库有很多坏处,这使得使用者可能会遇到版本冲突的问题(比如:友盟反馈和友盟推送同时使用),方法数也会极速增多。在当今时代,appcompat
这个库基本是所有第三方库都会引入的,有没有什么好的办法可以避免呢?
我们可以使用complieOnly
关键字,complieOnly
可以将你需要的库引入,但是并不会将其打包到你的代码里面。
以tianzhijiexian/commonAdapter为例:
dependencies {
complieOnly 'com.android.support:recyclerview-v7:23.2.1'
complieOnly 'com.android.databinding:baseLibrary:1.0'
complieOnly "org.projectlombok:lombok:1.12.6"
}
CommonAdapter依赖了三个库,都用了私有依赖,可以从中看到三个典型的例子:
作者能确定使用这个库的人,肯定使用了recyclerView。私有依赖方式可以将recyclerView的代码剔除,让recyclerView的版本由使用者来定
如果使用者的项目使用了dataBinding这个库,那么可以采用数据绑定的形式来做界面的更新操作。通过私有依赖,可以不强制使用者依赖dataBinding
为了增加代码的可维护性,有些项目会引入lombok,但这个库仅仅用来生成代码,对于使用者的项目应该没有任何影响,所以也进行了私有依赖
关于dataBinding方面这里要多说几句,我们无法知晓使用者是否用了dataBinding,所以这里的判断必须通过动态的反射来做:
public class DataBindingJudgement {
public static final boolean SUPPORT_DATABINDING; // 是否支持dataBinding
static {
boolean hasDependency;
try {
Class.forName("android.databinding.ObservableList");
hasDependency = true;
} catch (ClassNotFoundException e) {
hasDependency = false;
}
SUPPORT_DATABINDING = hasDependency;
}
}
通过这个判断,我们可以在相应的地方决定是否启用dataBinding的功能:
public CommonAdapter(@Nullable List<T> data, int viewTypeCount) {
if (DataBindingJudgement.SUPPORT_DATABINDING && data instanceof ObservableList) {
// ...
}
}
著名的rxAndroid是基于rxJava进行开发的,自身的更新速度远远慢于rxJava,它也会遇到类似于support库的问题。它的做法是在内部进行私有依赖,在文档中给出和rxJava一并依赖的要求,并且告诉使用者可以自行决定rxJava的版本。
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
// (see https://github.com/ReactiveX/RxJava/releases for latest 2.x.x version)
implementation 'io.reactivex.rxjava2:rxjava:2.x. // 2.x的写法
我们制作的库很可能会用到回调,我们希望给已经使用了rxjava的项目提供更优雅的回调,给没有使用rxjava的项目提供默认的接口回调。实现这个功能并不复杂,仅仅是利用了函数的多态性:
compileOnly 'io.reactivex:rxjava:1.1.3'
私有依赖rxJava后,下面写了两个share()方法,后者是支持rxJava的:
因为利用了多态,这里的代码并不需要反射判断使用者是否用到了rxjava。
DebugDrawer仅仅是一个debug库的壳子,使用者可以选择兼容的依赖库来扩展其功能。这些库引入的原因是因为debugDrawer,他们密切相关,所以开发者应该建议使用者将他们通过下面的写法依赖进来,这样以后阅读gradle文件和删除库的时候会很方便。
debugImplementation([
"com.github.tianzhijiexian:DebugDrawer:1.0.0",
"jp.wasabeef:takt:1.0.1",
"com.jakewharton.scalpel:scalpel:1.4.6"
])
题外话:
一个越容易引入的库,则越容易删除;越容易删除的库,则是越优秀的库。
很多代码仓库是支持用一个特殊关键字做版本号,类似的关键字为SNAPSHOT或latest.release。
dependencies {
implementation 'com.github.tianzhijiexian:SelectorInjection:master-SNAPSHOT'
}
这样做的好处是使用者可以永远不修改版本号,联网后就能享受到最新的库版本。为了方便实时更新,还有一些库建议每隔几小时检测下更新。
configurations.all {
resolutionStrategy.cacheDynamicVersionsFor 4, 'hours' // 这里是每隔4h同步一次
}
这种看似讨巧的做法严重的破坏了代码的严谨性,该做法有如下严重缺点:
Glide是我们很熟悉的一个图片库了,它的一个小功能是在activity被destroy的时候会自动停止图片的加载,解决内存泄漏和空指针回调的问题。
public static RequestManager with(Context context) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(context);
}
public static RequestManager with(Activity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
public static RequestManager with(FragmentActivity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
Glide实现这个功能的方案就是监听了生命周期,也就是说这个库和生命周期是密切相关的。在源码中我们发现,它其实利用的就是fragment来监听activity生命周期的策略。在开发开源库的时候,我们可以将activity或其hashCode作为一个请求,在监听到onDestroy事件的时候就可以针对性的进行资源的释放了。
那么当我们只能得到application的时候该怎么做呢?
在Leakcanary中,有一个RefWatcher对象。这个对象就是用来监听activity被销毁的事件的,而核心方法就是application提供的activityLifecycleCallbacks类。
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
public void onActivityStarted(Activity activity) {
}
// ...
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity); // 处理activity销毁时的逻辑
}
};
一般我们在做一些debug库的时候,可以多多考虑从application层面进行监听,减少使用者的负担。当然了,我们也不要忘记android最新提供的lifecycle组件,利用这个更加现代化的组件可以让很多事情事半功倍。
我们知道有很多库都需要初始化,而初始化的地方往往是application,下面是常见的代码:
public class MyApplication extends App {
@Override
public void onCreate() {
super.onCreate();
Zeus.getInstance().init(this, true);
}
}
而google的firebase有一个不同的做法,它不用我们做任何的配置,当我们在gradle引入它后,它就能自动运行了。
究其原因是fireBase利用了contentProvider来做初始化,所以不用开发者编写任何初始化的代码,但使用这个方案需要注意以下几点:
你的代码会初始化在activity、service、broadcastReceivers之前,所以不要随意引用一些对象,但可以持有全局的context。
在manifest合并时,会将你库中的contentProvider和使用者的manifest进行合并,而manifest中的contentProvider会在app 按照优先定义的顺序来创建。
ContentProvider是运行在主进程的,所以需要在readme中说明这点。如果使用者开发的是多进程的应用,那么应提供其他的方案进行初始化。
这种自动初始化其实没有利用contentProvider的任何跨进程功能,大多方法是return null
的。强烈建议用这种方法的库作者在代码中添加注释,阐述该类的意图。
定义的例子:
<provider
android:authorities="${applicationId}.yourcontentprovider"
android:name=".YourContentProvider"
android:exported="false" />
有一个很好用的debug库——android debug database就用了这种机制,它无需我们编写手动初始化的代码,它会自动启动一个读取database的后台服务,所以文档也很简洁。
其中contentProvider的代码:
public class DebugDBInitProvider extends ContentProvider {
@Override
public boolean onCreate() {
DebugDB.initialize(getContext()); // 真正有用的初始化代码
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public void attachInfo(Context context, ProviderInfo providerInfo) {
if (providerInfo == null) {
throw new NullPointerException("DebugDBInitProvider ProviderInfo cannot be null.");
}
// So if the authorities equal the library internal ones, the developer forgot to set his applicationId
if ("com.amitshekhar.DebugDBInitProvider".equals(providerInfo.authority)) {
throw new IllegalStateException("Incorrect provider authority in manifest. Most likely due to a "
+ "missing applicationId variable in application\'s build.gradle.");
}
super.attachInfo(context, providerInfo);
}
}
重要说明:
这样的设计值得我们学习,但也引出了一个问题——一旦你的库在初始化阶段有了任何的异常,使用者是没有办法停止它的。这种方案仅仅给使用者“用”或“不用”的选择,没有提供暂时关闭的可能。
当你的库和别的库产生了冲突,或者用户想要暂停或延迟初始化的话,这种方案就成了灾难的根源,很难解决。除非你能保证你的库不会引起任何的冲突,否则请不要使用这种方案。
如果你的库代码仅仅需要出现在debug模式中,并且对于使用者现有的代码没任何影响,那么你可以建议使用你的库的人通过debugCompile
进行依赖,并在readme中写明配置的方法。
以stetho为例,我们不依赖no-op的版本:
debugImplementation "com.facebook.stetho:stetho:1.3.1"
依赖后,记得在src下建立debug/java的目录,接着建立一个debugApplication的类:
public class DebugApplication extends ReleaseApplication {
@Override
public void onCreate() {
super.onCreate();
Stetho.initialize(
Stetho.newInitializerBuilder(this)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(this))
.enableWebKitInspector(
Stetho.defaultInspectorModulesProvider(this)).build());
}
}
最后,不要忘了在debug目录下的manifest文件中进行application的替换:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:name=".DebugApplication"
android:allowBackup="true"
android:icon="@drawable/debug_icon"
tools:replace="android:name,android:icon"
/>
</manifest>
这样我们就仅仅会在debug时用debugApplication作为application对象,使用stetho的代码,以此来减少无用代码的引入。
如果你开发的库只在开发环境才用到,但库提供的类或方法会被生产环境依赖,那么就可以采用no-op的方案了。所谓no-op,就是仅仅在debug时才依赖真的实现,在release版本中不依赖具体实现类,自然没有额外方法的引入了。
以leakcanary为例:。
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
有些库会启动一个后台服务,通过这个服务来完成某些特定的功能,大家了解的推送sdk就是这样的库。也有一些库是在手机上启动一个service,用来将手机作为服务器。
public class LogcatService extends IntentService {
private static final String TAG = "LogcatService";
// ...
对于这些库,我们有必要提供开启,关闭,是否正在运行这三个方法。很多库的开发者忽略了是否运行这个模式,没有提供检测的方法。这就让使用者必须通过反射等方案才能知道代码的状态,这需要库开发者特别注意一下。
对于此类库,必须提供的配套方法有:
非必须提供的方法有:
Jitpack提供了java文档的在线浏览功能,如果你的库需要提供文档支持,那么它绝对是一个很好的选择。
配置的方式是在lib的build.gradle中添加如下代码:
// build a jar with source files
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
failOnError false
source = android.sourceSets.main.java.sourceFiles
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += configurations.compile
}
javadoc {
options {
encoding "UTF-8"
charSet 'UTF-8'
author true
version true
links "http://docs.oracle.com/javase/7/docs/api"
}
}
// build a jar with javadoc
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
接着把代码push到github上后我们就可以在线浏览文档了:
如果你想详细了解jitpack,可以查看jitpack的官方文档。
一个好的介绍文档对于项目的传播和理解是至关重要的,如果你希望项目走向世界则你可以用全英文文档;如果你仅仅希望在国内推广,那么可以用国人友好的中文作为写作语言。一般情况下,我们都会把主要的依赖方式和重要的使用说明放在文档的头部,将license这类的信息放在底部。
1.头部介绍
如果是复杂的项目,那么推荐在头部区域做大量的说明,让使用者可以很快明白这个项目的开发意图。对于简单的项目,一般的建议是用一句话概要的说明即可,然后紧接着提供使用说明。
添加依赖的写法是有模板的,比如:
1.添加JitPack仓库
repositories {
maven {
url "https://jitpack.io"
}
}
2.添加依赖
implementation 'com.github.tianzhijiexian:SelectorInjection: Latest release ( <- click it )'
如果你想了解如何把项目logo和说明进行组合,harjot-oberai/MaterialShadows就是一个很优秀的示例:
题外话:
如果项目的readme中有gif,那么请务必进行压缩,过大的gif会影响静态网页的打开速度。即使项目再出众,如果连readme都打不开,自然就没人想用你的项目了。
2. 底部说明
底部一般放置的是开发者编写的项目许可,许可证可以放在另一个文档中或直接贴在下方,比较不建议在底部直接放打赏的二维码,会影响整个项目的气质,建议新开一页或将二维码放在底部的最后一行。
MaterialShadows也有着简洁的底部说明:
3. 正文内容
文档中最重要的、也是差异最大的就是正文部分了,正文的写法因项目的不同而不同。一般来说内容介绍要简单扼要,如果内容过多,可以放在gitBook中进行详细的阐述。
很多自定义view的项目总是喜欢将各种属性写到readme中,这显得十分杂乱,下面就是一个不推荐的写法:
<!-- 普通状态的颜色 -->
<attr name="normalColor" format="color" />
<!-- 按下后的颜色 -->
<attr name="pressedColor" format="color" />
<!-- 选中后的颜色 -->
<attr name="checkedColor" format="color" />
<!-- 常规状态下-->
<attr name="normalDrawable" format="reference" />
<!-- 按下/获得焦点 -->
<attr name="pressedDrawable" format="reference" />
<!-- 选中时 -->
<attr name="checkedDrawable" format="reference" />
<!-- 正常的描边 -->
<attr name="normalStrokeColor" format="color" />
<attr name="normalStrokeWidth" format="dimension" />
<!-- 按下后的描边 -->
<attr name="pressedStrokeColor" format="color" />
<attr name="pressedStrokeWidth" format="dimension" />
<!-- 选中后的描边 -->
<attr name="checkedStrokeColor" format="color" />
<attr name="checkedStrokeWidth" format="dimension" />
更加优雅的写法是将效果和属性通过表格的形式进行展现,这样更加直观。
ShapeButton | Explain | Sample |
---|---|---|
Enable | app:radius="1.5dp" app:normalStrokeWidth="0.2dp" app:normalColor="@color/green" |
|
Disable | android:enabled="false" app:radius="1.5dp" app:normalStrokeWidth="0.2dp" app:normalColor="@color/green" |
|
Enable | app:radius="1.5dp" app:normalColor="@color/green" |
|
Disable | android:enabled="false" app:normalStrokeWidth="1.5dp" app:normalColor="@color/green" |
上述表格参考自:CustomUI
比如某个自定义view项目支持了textView上下左右的图片设置,这些属性如果全部描述出来就是:
app:drawableBottom="@drawable/icon_facebook_svg" // 底部的图片
app:drawableBottomTint="@color/green" // 底部图片的tint
app:drawableLeft="@drawable/icon_facebook_svg"
app:drawableLeftTint="@color/red"
// ...
这种写法其实本身没有什么问题,但更推荐通过说明文档的方式进行阐述,比如:
SelectorTextView:(设置左侧icon的示例)
app:drawableLeft="@drawable/icon_facebook_svg"
app:drawableLeftTint="@color/red"
支持上下左右四个区域的icon,属性格式:
这样,通过前缀和后缀的描述,使用者可以很快的通过两个属性的介绍知晓该view的功能。
jitpack
jitpack可以通过插入link的方式来自动获得jitpack上的最新版本,这样使用者可以通过readme中的标签知道当前最新的库版本了。
我们要做的就是进入jitpack,然后选择自己库最新的版本,最后将图片链接复制到readme中。
在线示例:https://jitpack.io/#tianzhijiexian/Shatter/1.0.8
shields.io
项目地址:http://shields.io/
Shields是一个提供静态标签资源的网站,这个网站收入了花样繁琐的标签,我们常见的license或者downloads都可以在这里找到。
progressed.io
项目地址:https://github.com/fehmicansaglam/progressed.io
这是一个提供进度条标签的项目,如果你开源的是一个多人协作的工程,那么可以考虑使用它。它能提供任务的进度,可以在readme中展示当前的版本开发进度,比如当前库正在开发第二版本。
一个常见的用法是master和dev同时开发,开发者可以在master上通过这个标签更新dev版本的进度,美观又醒目。
简介标签
很多作者为了保证文档的简洁,会把一些技术点和使用语言放在简介标签中,将使用者不关心但有一定意义的内容从readme中移除,保证了主体文档的干净。
在evernote/android-state这个项目中,它将用到的语言和技术都进行了说明,阅读起来还是很直观的。
JCenter是全世界最大的java仓库,也是android studio中repositories的默认仓库,但因为它的审核速度和易用性让很多人更倾向于使用jitpack。
Jitpack是一个高度兼容github的代码仓库。我们的代码大多都是存在github上面,jitpack可以快速将你的github项目变成可以被使用者进行依赖的库。它会把作者名和项目名作为path,你会得到一个属于你自己的项目网站,比如:https://jitpack.io/#tianzhijiexian/Shatter(作者名称/项目名称)。
它允许我们通过tag和commit进行库版本的选择,选择完毕后可以立刻看到依赖的配置,复制即可。
SourceGraph
Github一个不好的地方就是代码是不能相互跳转,阅读起来很累。如果我要引入一个库,就必须clone到本地并通过IDE打开才能方便的浏览。
这样的流程对于库的前期调研来说成本过高,推荐大家使用SourceGraph这个chrome插件,它会让在线阅读代码的体验提升一个量级。在安装完sourceGraph后,你会发现支持sourceGraph的代码上方会显示一个icon。现在,你可以试试“鼠标悬停出注释”或是“点击跳转到变量定义处”的功能了。
Insightio
Insightio是一个chrome插件,安装完毕后可以在屏幕上显示侧边栏。在侧边栏中它已经为我们建立好了树形视图,在这个视图中我们甚至可以进行搜索。
一个优秀的开源库自然要经历很多issue,作为库开发者需要对issue有一定的敏感度,不要因为自己太忙而放任不管。
个人有如下的经验:
在大型的项目中issues很可能达到几百个,如何管理issues就是一个问题了,这时候issue标签就派上了用场。
在上图的issues中,我们可以很方便的看到是版本问题还是平台问题,比较直观。在issues的label区域有个单选框,我们可以在这里找到常用的标签,如果对于标签或标签的内容不满意,还可以自行编辑。
一个库的demo可以帮助使用者迅速了解到库的功能和效果,但是每次编译demo是十分费时的。
cesarferreira/dryrun的功能是自动下载、编译、安装你想要测试的项目demo,可以方便我们快速查看第三方库的实际使用效果,对于自定义view等项目比较有用。
安装方案:
gem install dryrun
支持的命令:
$ dryrun -h
Usage: dryrun GIT_URL [OPTIONS]
Options
-m, --module MODULE_NAME Custom module to run
-b, --branch BRANCH_NAME Checkout custom branch to run
-f, --flavour FLAVOUR Custom flavour (e.g. dev, qa, prod)
-p, --path PATH Custom path to android project
-t, --tag TAG Checkout tag/commit hash to clone (e.g. "v0.4.5", "6f7dd4b")
-c, --cleanup Clean the temporary folder before cloning the project
-w, --wipe Wipe the temporary dryrun folder
-h, --help Displays help
-v, --version Displays the version
-a, --android-test Execute android tests
对于一个项目来说,通常会有很多次pull request,很有可能因为提交者忽略了一些东西而引起整个项目build失败。对于这种情况,很多专业的github项目已经引入了持续集成框架,用这个框架来保证每次提交的合法性。
比如给twitter提交代码的时候,它首先会自动检测是否能无冲突的被merge,其次会自动触发一次build,只有build成功后才会认为当前pr达到了可被merge的状态,十分的优雅。
作为开源库的作者,我们也可以学习一下类似的思路,引入CI工具。一个比较知名的工具就是travis CI,它可以提供的支持有很多种,比如云端自动化测试、每次提交后自动触发测试用例等等,大家可以自行研究一下。
Travis的地址:https://travis-ci.org/
完善和维护一个库确实需要很大的精力,如果你的库是真的希望给更多人用的,那么你就应该有付出时间和精力的准备。你既然做了这件事,那么就需要为此负责,否则可以在文档中注明“已废弃”或“学习项目”的字样。
通过本篇的叙述相信大家不仅知道了如何做一个开源项目,也知道了定义优秀开源项目的标准,相信本文可以在未来的开发过程中帮助大家选取更加合适自己的库。