android SDK 开发心得笔记

这篇博文博主在心里酝酿了好久了,在从事SDK开发的时候 从最初的版本到现在还未完 成的版本,算是收获良多,本篇博文就把自己的心得体会记录下来,算是个总结吧,估计篇幅不少,博主会尽量组织的合理点来做说明。

闲言少叙,开始发车。

项目组开发的SDK的从外观体现来看(抛开了具体的业务不谈)其核心原理就是动态的向一个ViewGroup里面添加(addView)和删除(remove)View的过程。

最早版本的SDK是在eclipse上开发的,每次都是以jar的形式混淆过后发布,后来入职不久以后,被博主改成了使用Android Studio,虽然不能说是鸟枪换炮,但也比Eclipse好用多了。

SDK的多元化模块的进化历程:
最初版本的SDK就是一个大的模块:也就是一个project或者module下根据包名来区分各个包的功能:
在一个Eclipse 的Project下分成形如cn.com.xxx.net、cn.com.xxx.img、cn.com.xxx.util、cn.com.xxx.view等看起来很合理的分包方式;但是呢很显然这种方式有一个很大不足,就是代码的复用性价值不大;比如两个不同的项目因为有两个相似的功能,比如都需要图片库和网络库的功能,要如何做呢?最初的做法是A代码中的net和img包下的代码通过ctrl+c/ctrl+v的方式整体复制到B项目中使用,现在想来真是what the fuck!

除了硬性的复制粘贴这个弊端之外,还有一个弊端:图片库/网络库都是使用的github现成的库Glide/OKhttp, 这样整个项目中用到图片的地方充斥着Glide.with和OkhttpClient.newCall等类似的代码。也就是说项目中的代码与Glide和OKhttp等第三方库耦合的太严重了!作为SDK提供方这会埋下很大的坑:如果使用我们SDK的客户,他们在自己的app内使用的图片库和网络库跟我们SDK的不一样,且对方不愿意用Glide/Okhttp库,并强烈要求我们SDK可以替换成他们的图片或者网络库的功能该怎么办?这样我们的SDK就必须不得不去掉检索代码中的关于Glide和OKhttp的相关代码并且进行删除替换,重新打包,这是多么庞大的任务量!而且SDK使用方不止一家,那么要是针对每一家都出创建不同的git 分支,这样代码的维护成本将会大大提升。怎么解决这一方法呢?

这其实也算是前期思考或者设计方面投入的精力不足导致的,只想着怎么快速去实现功能就万事大吉,而没有从设计角度进行深入的思考,当拿到一份需求的时候就想着立马去动手敲代码去实现它,而不是花一点精力去构思和抽象化,这也算是个小小的经验教训吧(but ,貌似每次来的都是紧急需求啊,压得你根本没法来得及思考,算是个吐槽)。

其实问题的核心就是解耦!为此新版的SDK设计图片库和网络库的接口(IImageLoader/IHttpConnect),SDK本身也实现了这两个接口,在代码中使用Glide.with的地方全部改成IImageLoader。如果SDK使用方想把他们自己的图片库和网路库的功能嵌入到我们的SDK中,只需要实现IImageLoader/IHttpConnect这两个接口,并且注入到我们的SDK中即可替换SDK中默认的实现。其思想可以看作策略模式的体现。

以Glide为例重构之前的加载图片的代码:

 Glide.with(context).load(url).into(imageView)

重构之后将上面的代码替换成 类似如下的代码:

 imageLoader.load(imageView,url,callback)

其中imageLoader为IImageLoader接口的实现类,具体实现SDK中默认是glide,当然也可以让客户端自己注册来实现自己的图片加载框架,这样SDK只关注怎么使用图片库,而不关心具体是哪一个图片库,也就是说不必关心具体的实现细节,这样SDK就完成加载图片的功能,也算是一定程度上满足了类的开闭原则,更是满足了要依赖于抽象,不要依赖于具体的实现的设计原则

重构之前的项目结构:
android SDK 开发心得笔记_第1张图片

重构之后的项目结构:
android SDK 开发心得笔记_第2张图片
重构后的代码结构就是有一个Project改成了Project+若干个module的形式:右边剪头指向的就是一个个module。

其实如果你阅读或者使用其他第三方库源码的话,他们也是这么做的,对外暴露接口,以便允许用户提供自己的默认实现。比如Okhttp/Volley 配置自定义缓存策略;glide使用Okhttp代替内部的HttpUrlConnection等等等,此类的设计不胜枚举。

分成若干个module之后又面临打包的问题,不可能把每一个module都打包成aar,然后让用户在build.gradle里面写多个compile依赖多个aar;所以又调研了下合并aar的方案,最终利用fat-aar将模块进行合并打包成一个对外发布的aar。打包发布aar,涉及到了自动构建的的问题,说白了就是脚本自动跑git pull,然后运行gradle assembleRelease 等命令将aar发布到jcenter上面的过程。具体的过程可参考此篇博文

SDK中设计模式的应用

纵观整个SDK,涉及到了如下设计模式:Builder模式,工厂模式,状态模式、观察者模式、适配器模式、单利模式,当然还有一些模式的变种等等,这些模式都是经常被使用的模式。
下面简单的介绍下几个模式在SDK中得使用场景:

Builder模式:在上图的ImageLoader接口中创建加载图片得信息:比如图片url,placeHolderDrawable,errorDrawable,resizeWidth/resizeHeight等等都是利用构建者模式来完成的,这点跟ImageLoader或者Glide得配置方式一样使用得Builder模式。还就就是构建Http得Request,该对象涉及到Request 得URL,Headers,Params, 充实次数,读写超时时间等等信息,利用Builder模式很适合。博主以为构建模式的最大优点就是:使得目标对象状态的不可变成为了可能。

工厂模式:看上图,SDK中可以使用不同的图片加载库和网络请求库,这些不同库的对象的创建就是利用反射+工厂模式来构建一个具体的ImageLoader和HttpClient对象的,比如ImageLoader,大致代码如下。

static ImageLoader createImageLoader() {
//获取客户端配置的图片加载库:可能是Glide,Fresco等库
   Classextends IImageLoader> imageLoaderConfig.getImageLoaderLib();
      return imageLoader.newInstance();
}

状态模式:一个app或者SDK都有配置不同的环境:正式,测试,预发环境,不同的环境访问不同的接口或者其他操作:比如测试环境下显示Log输出等。通常一个app都有后门来切换不同的环境:在一个很隐秘的地方或者以很隐秘的方法来提供一个切换环境的入口。这种情况下很适合用状态模式来实现。当然实现环境切换的方式多了去了,只不过模式只是解决问题的方案之一罢了。

适配器模式:适配器模式的主要意图是将一个类的接口转换成客户希望的另外一个接口,但是不要被其定义所限制住,设计模式的核心思想掌握了,就算是从代码形式上看不是某个设计模式,但是思想一样,也可以看作是设计模式灵活利用的提现。在上文中我们知道SDK既可以利用Glide 也可以利用Fresco来完成图片的加载,但是Fresco有点不同的的地方就是其利用的是SimpleDraweeView这个ImageView的子类来完成图片的加载。所以SDK为了方便的切换Glide和Fresco不得不作出适配。

方法是SDK除了提供ImageLoader接口,还提供了IImageView接口:

//图片加载接口
public interface IImageLoader {

    void loadImage(WeakReference imageView, VenvyImageInfo venvyImageInfo, @Nullable IImageLoaderResult result);

    void preloadImage(Context context, VenvyImageInfo venvyImageInfo, @Nullable IImageLoaderResult result);
}

//图片接口
public interface IImageView {
    void setImageBitmap(Bitmap bitmap);

    void setImageDrawable(Drawable drawable);

    void setImageURI(Uri uri);

    void setScaleType(ScaleType scaleType);

    void setLayoutParams(ViewGroup.LayoutParams params);

    Context getContext();

    ImageView.ScaleType getScaleType();

    View getImageView();

    void setImageResource(int resourceId);
}

需要注意的是上面代码中IImageView接口提供的方法除了getImageView方法,其余的都是ImageView提供的方法,名字参数都是照搬过来的。这样我们就复写SimpleDrawee,让其实现这个接口:

class FrescoImageView extends SimpleDraweeView implements IImageView {
    public VenvyFrescoImageView(Context context) {
        super(context);
    }

    @Override
    public View getImageView() {
        return this;
    }

    public void setScaleType(ImageView.ScaleType scaleType) {
        if (scaleType == null) {
            return;
        }
        ScalingUtils.ScaleType tempScaleType = null;
        switch (scaleType) {
            case CENTER_INSIDE:
                tempScaleType = ScalingUtils.ScaleType.CENTER_INSIDE;
                break;

            case FIT_XY:
                tempScaleType = ScalingUtils.ScaleType.FIT_XY;
                break;

            case FIT_START:
                tempScaleType = ScalingUtils.ScaleType.FIT_START;
                break;

            case FIT_CENTER:
                tempScaleType = ScalingUtils.ScaleType.FIT_CENTER;
                break;

            case FIT_END:
                tempScaleType = ScalingUtils.ScaleType.FIT_END;
                break;
            case CENTER:
                tempScaleType = ScalingUtils.ScaleType.CENTER;
                break;
            case CENTER_CROP:
                tempScaleType = ScalingUtils.ScaleType.CENTER_CROP;
                break;
        }

        GenericDraweeHierarchy hierarchy = getHierarchy();
        if (hierarchy == null) {
            GenericDraweeHierarchyBuilder hierarchyBuilder =
                    new GenericDraweeHierarchyBuilder(getResources());
            hierarchyBuilder.setActualImageScaleType(tempScaleType);
            hierarchy = hierarchyBuilder.build();
            setHierarchy(hierarchy);
        } else {
            hierarchy.setActualImageScaleType(tempScaleType);
        }

    }

同样的也要复写ImageView,让其也即成IImageView接口:

DefaultImageView extends ImageView implements IImageView {
    public DefaultImageView(Context context) {
        super(context);
    }

    public View getImageView() {
        return this;
    }
}

这样就把Imageview看作是IImageView来使用,然后把SDK代码中写死的ImageView img = new ImageView(context)代码换成

/*利用工厂模式生成一个IImageView对象*/
IImageView imge = ImageViewFactory.createImageView()

将image交给IImageLoader 完成图片的加载。其中IImageView对象的产生是通过工厂模式来完成的,默认情况下始终的是上面的DefaultImageView(即原生的ImageView),。如果客户端使用的是Fresco图片库的话,那么该工厂模式创建的对象就是上述代码中的FrescoImageView了。就这样就完成了适配工作。

这样算是体现了依赖于抽象,不依赖于细节的优点。

(未完待续)

你可能感兴趣的:(android)