Android多渠道SDK开发心得(5)——多渠道sdk的调试

一.多渠道sdk的验证

1.新增库验证

新建http库工程,仅包含如下一个类NetManager.java
http/src/main/java/com.tobenull.http.NetManager.java

package com.tobenull.http;

import android.util.Log;

/**
 * Created by tobenull on 11/27/17.
 */

public class NetManager {
    private static NetManager instance = new NetManager();
    private NetManager(){}
    public static NetManager getInstance() {
        return instance;
    }

    public void init() {
        Log.d("null-tobe", "NetManager->init");
    }
}

修改各个库之间的依赖关系,如下

sdk->base->http

sdk/build.gradle

...
android {
    ...
}
dependencies {
    if (rootProject.ext.debug) {
        compile project(':base')
    } else {
        embedded project(':base')
        embedded project(':http')
    }
    ...
}

base/build.gradle

...
dependencies {
    ...
    compile project(':http')
}

在命令窗口输入以下命令

gradle clean main

解压sdk/build/outputs/aar目录下的sdk-alibaba-release.aar和sdk-baidu-release.aar并反编译其classes.jar,均如下:

v com.tobenull
    v base
        > BaseManager.class
        > BuildConfig.class
        > TLog.class
    v http
        > BuildConfig.class
        > NetManager.class
    v sdk
        > BaseSDK.class
        > BuildConfig.class
        > SDK.class

  而且两个渠道的SDK.class中内容分别为alibaba和baidu两个渠道的源码。

  经过以上的步骤,我们只是新增了一个http库并添加了依赖配置,并没有修改合并脚本相关的任何代码,最终构建得到了符合预期的sdk(将新增的http库也合并进来),由此可见,我们的合并脚本是没问题的。

2.新增渠道验证

修改sdk/build.gradle脚本,新增”tencent”渠道,如下
sdk/build.gradle

...
android {
    ...
    productFlavors {
        baidu{}
        alibaba{}
        tencent{}
    }
    ...
}
...

在命令窗口执行gradle clean main命令后,展开sdk/outputs/aar/目录

    v sdk
        v build
            ...
            v outputs
                v aar
                    ...
                    sdk-alibaba-debug.aar
                    sdk-alibaba-release.aar
                    sdk-baidu-debug.aar
                    sdk-baidu-release.aar
                    sdk-tencent-debug.aar
                    sdk-tencent-release.aar
            ...

解压sdk-tencent-release.aar包并反编译其中的classes.jar,如下

v com.tobenull
    v base
        > BaseManager.class
        > BuildConfig.class
        > TLog.class
    v http
        > BuildConfig.class
        > NetManager.class
    v sdk
        > BaseSDK.class
        > BuildConfig.class

  因为我们没有在sdk库中新建tencent的渠道,所以打出来的sdk中只有main的内容,如果有兴趣,可以试着仿照baidu或alibaba渠道新建tencent渠道并实现SDK类供第三方调用。

  经过上述步骤,我们在没有修改打版脚本的情况下只是新建了一个tencent渠道,执行构建命令就成功地生成了tencent渠道的aar包,说明我们的合并脚本是没有问题的。

二.多渠道sdk的调试

  经过前面的文章,我们已经完成了sdk多渠道开发与构建的工作,sdk的调试我们会在app工程中也以多渠道的方式进行。设置{rootProject}/build.gradle中的debug为true、采用正常的库工程依赖方式,当调试没问题时上传代码,服务端设置debug为false进行sdk的多渠道构建。
  在将{rootProject}/build.gradle中的debug设为true后,同步后会发现App.java中会报SDK找不到的提示,这是因为我们app工程只是依赖于sdk,没有设置对各个渠道的分别依赖,而SDK.java在sdk库的main中又不存在,需要对多个渠道进行单独依赖,如下:
app/build.gradle

dependencies {
    if (rootProject.ext.debug) {
//        compile project(':sdk')
        baiduCompile project(path: ':sdk', configuration: 'baiduRelease')
        alibabaCompile project(path: ':sdk', configuration: 'alibabaRelease')
        tencentCompile project(path: ':sdk', configuration: 'tencentRelease')
    } else {
        ...
    }
    ...
}

  在com.android.tools.build:gradle 3.0.0以前的版本,依赖默认使用的都是Compile,而多渠道中可以针对特定的渠道设置特定的依赖,如此处的baiduCompile等。
  由于sdk中有多个不同的渠道,所以在进行依赖时需要指定configuration,这样当我们切换app工程中的渠道时、其依赖的sdk也会自动切换到对应的渠道上来。

此时如果报以下错:

Error:Project :app declares a dependency from configuration 'alibabaCompile' to configuration 'alibabaRelease' which is not declared in the descriptor for project :sdk.

需要确保sdk库中配置多渠道的位置设置publishNonDefault为true
sdk/build.gradle

android {
    ...
    publishNonDefault true
    productFlavors {
        ...
    }
    ...
}

  同步成功后,我们就可以针对不同的渠道通过app工程的调用对各渠道sdk进行调试了。
  打开android studio的Build Variants窗口,默认显示如下:

        module        |        Build Variant
    app               |    alibabaDebug
    base              |    debug
    http              |    debug
    sdk               |    alibabaRlease

  运行app,打开日志过滤”null-“,demo运行日志如下:

12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: BaseSDK->init, alibaba
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: BaseManager->init
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: SDK->initAlibaba, init alibaba...
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: MapManager->init, init gaode map...

  点击”alibabaDebug”,切换为”baiduDebug”,重新运行app,日志如下:

12-16 14:49:40.882 11903-11903/? D/null-tobe: BaseSDK->init, baidu
12-16 14:49:40.882 11903-11903/? D/null-tobe: BaseManager->init
12-16 14:49:40.882 11903-11903/? D/null-tobe: SDK->initBaidu, init baidu...

  根据上述日志的变化,即可发现demo依赖的渠道已由alibaba变为了baidu.从而,我们可以使用app工程对多个渠道sdk进行调试,当调试通过后,上传库工程源码进行打版、生成可用的aar包。
  此外,我们也需要对app进行多渠道配置,main目录存放公共代码、各渠道中存放对应渠道的测试代码。这样一方面可以避免app测试工程在来回切换渠道时由于各渠道提供接口的不一致出现混乱,另一方面我们后续可以根据各渠道和main中的测试源码自动生成demo.

  为app工程建立多渠道包

  鼠标选中app/src目录、右键->New->Folder->Java Folder,点击Target Source Set下的main,选择alibaba,finish,即完成了alibaba渠道的创建。使用同样的方式建立baidu、tencent渠道。

  使用mvp模式,在main中定义统一的接口及view,在各渠道中各自实现相应的presenter.
代码如下:
app/src/main/java/com/tobenull/multiflavorsdk/App.java

package com.tobenull.multiflavorsdk;

import android.app.Application;
import com.tobenull.sdk.SDK;

/**
 * Created by tobenull on 11/25/17.
 */
public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        SDK.getInstance().init();
    }
}

app/src/main/java/com/tobenull/multiflavorsdk/interfaces/IMainView.java

package com.tobenull.multiflavorsdk.interfaces;

/**
 * Created by tobenull on 12/16/17.
 */
public interface IMainView {
    void showResult(String result);
}

app/src/main/java/com/tobenull/multiflavorsdk/interfaces/IMainPresenter.java

package com.tobenull.multiflavorsdk.interfaces;

import android.widget.BaseAdapter;

/**
 * Created by tobenull on 12/16/17.
 */
public interface IMainPresenter {
    BaseAdapter getAdapter();
    void onItemClick(int position);
}

app/src/main/java/com/tobenull/multiflavorsdk/MainActivity.java

package com.tobenull.multiflavorsdk;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import com.tobenull.multiflavorsdk.interfaces.IMainPresenter;
import com.tobenull.multiflavorsdk.interfaces.IMainView;

public class MainActivity extends AppCompatActivity implements IMainView {

    private IMainPresenter mainPresenter;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mainPresenter = new MainPresentImpl(this, this);
        textView = (TextView) findViewById(R.id.textview);
        ListView listView = (ListView) findViewById(R.id.listview);
        listView.setAdapter(mainPresenter.getAdapter());
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView parent, View view, int position, long id) {
                mainPresenter.onItemClick(position);
            }
        });
    }

    @Override
    public void showResult(String result) {
        textView.setText(result);
    }
}

MainActivity布局如下:
app/src/main/res/layout/activity_main.xml


<LinearLayout
    xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:tools = "http://schemas.android.com/tools"
    android:layout_width = "match_parent"
    android:layout_height = "match_parent"
    android:orientation="vertical"
    tools:context = "com.tobenull.multiflavorsdk.MainActivity">
    <ListView
        android:id = "@+id/listview"
        android:layout_width = "match_parent"
        android:layout_height = "0dp"
        android:layout_weight = "1"
        tools:ignore = "Suspicious0dp" />
    <ScrollView
        android:layout_width = "match_parent"
        android:layout_height = "0dp"
        android:layout_weight = "1"
        tools:ignore = "Suspicious0dp">
        <TextView
            android:id = "@+id/textview"
            android:layout_width = "wrap_content"
            android:layout_height = "wrap_content"
            android:layout_centerInParent = "true"
            android:text = "Hello World!" />
    ScrollView>
LinearLayout>

渠道实现如下:
app/src/alibaba/java/com/tobenull/multiflavorsdk/MainPresentImpl.java

package com.tobenull.multiflavorsdk;

import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Toast;
import com.tobenull.multiflavorsdk.interfaces.IMainPresenter;
import com.tobenull.multiflavorsdk.interfaces.IMainView;
import com.tobenull.sdk.SDK;

/**
 * Created by tobenull on 12/16/17.
 */
public class MainPresentImpl implements IMainPresenter {

    private Context context;
    private IMainView mainView;
    private BaseAdapter adapter;

    public MainPresentImpl(Context context, IMainView mainView) {
        this.context = context;
        this.mainView = mainView;
        if (adapter == null)
            adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[] {
                    "SDK.getInstance().init();",
                    "SDK.getInstance().buy(product);",
            });
    }

    @Override
    public BaseAdapter getAdapter() {
        return adapter;
    }

    @Override
    public void onItemClick(int position) {
        StringBuffer result = new StringBuffer();
        switch (position) {
            case 0:
                SDK.getInstance().init();
                result.append("调用了SDK.getInstance().init();方法");
                break;
            case 1:
                String product = "alibaba debug demo";
                SDK.getInstance().buy(product);
                result.append("调用了SDK.getInstance().buy(product);方法\nproduct = " + product);
                break;
        }
        mainView.showResult(result.toString());
    }
}

运行demo后,依次点击init和buy的测试方法,日志如下:

12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: BaseSDK->init, alibaba
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: BaseManager->init
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: SDK->initAlibaba, init alibaba...
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: MapManager->init, init gaode map...
12-16 02:55:36.271 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: SDK->search, you want to buy alibaba debug demo

  同样的,在Build Variant窗口中切换渠道为baiduDebug,实现IMainPresenter接口如下:
app/src/baidu/java/com/tobenull/multiflavorsdk/MainPresenterImpl.java

package com.tobenull.multiflavorsdk;

import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Toast;
import com.tobenull.multiflavorsdk.interfaces.IMainPresenter;
import com.tobenull.multiflavorsdk.interfaces.IMainView;
import com.tobenull.sdk.SDK;

/**
 * Created by tobenull on 12/16/17.
 */
public class MainPresentImpl implements IMainPresenter {

    private Context context;
    private IMainView mainView;
    private BaseAdapter adapter;

    public MainPresentImpl(Context context, IMainView mainView) {
        this.context = context;
        this.mainView = mainView;
        if (adapter == null)
            adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[] {
                    "SDK.getInstance().init();",
                    "SDK.getInstance().search(key);",
            });
    }

    @Override
    public BaseAdapter getAdapter() {
        return adapter;
    }

    @Override
    public void onItemClick(int position) {
        StringBuffer result = new StringBuffer();
        switch (position) {
            case 0:
                SDK.getInstance().init();
                result.append("调用了SDK.getInstance().init();方法");
                break;
            case 1:
                String key = "baidu debug demo";
                SDK.getInstance().search(key);
                result.append("调用了SDK.getInstance().search(key);方法\nkey = " + key);
                break;
        }
        mainView.showResult(result.toString());
    }
}

运行app,日志如下:

12-16 03:03:49.400 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: BaseSDK->init, baidu
12-16 03:03:49.400 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: BaseManager->init
12-16 03:03:49.400 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: SDK->initBaidu, init baidu...
12-16 03:03:50.970 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: SDK->search, you want to search baidu debug demo

  经过上述步骤,我们现在已经实现了在同一个app工程下对多个渠道sdk进行调试。当调试无误后,即可上传库工程源码进行打版。

三.多渠道sdk的优化

  经过前面的介绍,我们现在已经可以得心应手地进行多渠道sdk的构建、开发和调试了。现在如果需要新建一个渠道,我们需要在sdk和app工程中分别进行配置,然后分别新建渠道代码,如果能通过一处进行统一管理就堪称完美了。

  构建脚本中一共有三处配置需要灵活处理,如下:
sdk/build.gralde

android {
    ...
    productFlavors {
        baidu{}
        alibaba{}
        tencent{}
    }
    ...
}

app/build.gradle

android {
    ...
    productFlavors {
        baidu{
            applicationId 'com.baidu.demo'
        }
        alibaba{
            applicationId 'com.alibaba.demo'
        }
        tencent{
            applicationId 'com.tencent.demo'
        }
    }
    ...
}

app/build.gradle

dependencies {
    ...
        baiduCompile project(path: ':sdk', configuration: 'baiduRelease')
        alibabaCompile project(path: ':sdk', configuration: 'alibabaRelease')
        tencentCompile project(path: ':sdk', configuration: 'tencentRelease')
    ...
}

  我们可以将渠道包信息在{rootProject}/build.gradle中统一定义,然后三处分别动态添加即可。
  首先在rootProject中定义一组配置信息,每个配置均有product、packageName和type三个属性,如下:
build.gradle

buildscript {
    ext {
        ...
        SDKProperties = [
                [product: 'alibaba',  packageName: 'com.alibaba.demo', type: 'jar'],
                [product: 'baidu',    packageName: 'com.baidu.demo',   type: 'aarjar'],
                [product: 'tencent',  packageName: 'com.tencent.demo', type: 'aar'],
        ]
        ...
    }
    ...
}

  以上属性中product作为渠道的标志,packageName为该渠道对应demo的包名,type属性暂时不用、作为后续生成jar、aar或jar+aar三种sdk产品的标记。

  动态添加渠道
sdk/build.gradle

android {
    ...
    productFlavors {
        rootProject.ext.SDKProperties.each {
            productFlavors.create(it.product)
        }
    }
    ...
}

app/build.gradle

android {
    ...
    productFlavors {
        rootProject.ext.SDKProperties.each {
          productFlavors.create(it.product).setApplicationId(it.packageName)
        }
    }
    ...
}

  通过rootProject.ext.SDKProperties.each遍历SDKProperties中配置的渠道包信息,productFlavors.create()动态添加渠道包,app工程中创建好渠道包后通过setApplicationId()即可设置该渠道对应demo的包名。
  注:此处的it代表的是SDKProperties中的每一项配置([product: ‘xxx’, packageName: ‘com.xxx.demo’, type: ‘xxx’])

  动态配置依赖
app/build.gradle

dependencies {
    if (rootProject.ext.debug) {
        rootProject.ext.SDKProperties.each {
            println "configurations: ${it.product}Compile"
            dependencies.add("${it.product}Compile", project(
                    path: ':sdk', configuration: "${it.product}Release")
            )
        }
    } else {
        ...
    }
    ...
}

  与前文写法类似,gradle可以通过”${it.product}Compile”动态生成alibabaCompile、baiduCompile等字符串然后使用dependencies.add()的方式添加相应的依赖

  经过上面的处理,我们可以只通过{rootProject}/build.gradle中的SDKProperties对渠道包进行管理。比如,现在需要新增一个product为iqiyi、包名为com.iqiyi.demo的渠道,则只需在SDKProperties中新增一行即可。
  如果iqiyi和baidu渠道类似,我们还可以通过脚本自动复制baidu的渠道代码、然后再次基础上进行进一步的开发,类似以下的处理方式:
sdk/build.gradle

rootProject.ext.SDKProperties.each {
    def product = it.product
    if (it.packageName == "" || it.packageName.length() <= 0)
        return
    // 如果渠道对应的sdk源码包不存在,则从base复制一份,根据base进行修改
    if (!file("src/$product").exists()) {
        copy {
            from "src/base"
            into "src/$product"
        }
    }
    // 如果渠道对应的demo源码包不存在,则从base复制一份,根据base进行修改
    if (!file("../app/src/$product").exists()) {
        copy {
            from "../app/src/base"
            into "../app/src/$product"
        }
    }
}

  在sdk开发、特别是多渠道sdk开发时,最好建立一个通用的base渠道,通过以上方式会在gradle同步时自动复制base渠道的源码到新建的渠道、新渠道也可以在添加后立马编译执行。
  此处我们以baidu渠道作为base渠道进行演示。

  新建iqiyi渠道,如下:
build.gradle

buildscript {
    ext {
        ...

        SDKProperties = [
                ...
                [product: 'iqiyi',  packageName: 'com.iqiyi.demo', type: 'aar'],
        ]
        ...
    }
    ...
}

  同步gradle成功后,在Build Variants窗口将渠道切换为iqiyiDebug,运行demo,日志如下:

12-16 04:57:54.581 32194-32194/? D/null-tobe: BaseSDK->init, baidu
12-16 04:57:54.581 32194-32194/? D/null-tobe: BaseManager->init
12-16 04:57:54.589 32194-32194/? D/null-tobe: SDK->initBaidu, init baidu...

  由此,我们实现了通过一行配置信息自动管理多渠道包的配置。

四.总结

  本文首先分别通过新增库依赖和新增渠道、在不改变构建脚本的情况下,生成了符合要求的sdk包,验证了前文sdk多渠道构建脚本的正确性。
  接着对多渠道调试进行了探究,通过配置多渠道app工程对多渠道sdk进行了本地调试、通过Build Variants窗口进行各渠道之间的切换。
  最后对多个渠道的配置进行了优化,通过动态添加渠道信息、动态添加依赖、动态复制源码的方式,使得多渠道的开发、调试与管理变得更加简单,只需一行配置信息即可进行控制。
  至此,关于多渠道sdk的开发、构建与调试心得的介绍也就基本结束了。后续将着重介绍自动生成demo及文档等内容,使得sdk的开发更加标准化,不用再费力地管理demo和文档了。

你可能感兴趣的:(Android,Gradle,aar,sdk)