为了账号安全,请及时绑定邮箱和手机立即绑定

android:一步步实现插件化与热更新(一)

标签:
Android

由于插件化开发与热更新最近貌似越来越火,新开的项目准备也使用插件化进行开发!其中遇到不少坑,在这里写了一个小的例子,记录一下开发流程,有助于自己,同时希望能够帮助大家理解,并且对于自身项目接入插件化有所帮助!

插件化

效果:
图片描述

插件化开发的含义:

插件化开发也是将一个项目app拆分成多个模块,这些模块包括宿主和插件。
每个模块相当于一个apk,而组件化相当于一个lib。
最终发布的时候将宿主apk和插件apk单独打包或者联合打包。

插件化的作用

  • 有效解决方法数超过了一个 Dex 最大方法数 65535 的上限

  • 模块解耦

  • 动态升级

  • 高效并行开发(编译速度更快)

  • 按需加载,内存占用更低

  • 节省升级流量

  • 易于维护

  • 易于团队开发

  • 易于功能扩展

使用框架

这里主要使用small框架实现插件化的,为啥我选择使用small
  • 它目前作者还在进行维护

  • 功能强大(请看下图你就会明白)
    图片描述
    图片描述
    图片描述

开始一步步实现插件化

  • *****首先你需要引入small**
需要在根目录下的build.gradle脚本里引入:
buildscript  {
    dependencies {
        classpath 'net.wequick.tools.build:gradle-small:1.3.0-beta3'
    }
}

apply plugin: 'net.wequick.small'

small {
    aarVersion = '1.3.0-beta3'
    buildToAssets = false
    android {
        compileSdkVersion = 26
        buildToolsVersion = "25.0.2"
        supportVersion = "25.1.0"
    }
}
  • *****新建插件模块****
File->New->Module来创建插件模块,需要满足:

    模块名形如:app.*, lib.*或者web.*
    包名包含:.app., .lib.或者.web.

    为什么要这样?因为Small会根据包名对插件进行归类,特殊的域名空间如:“.app.” 会让这变得容易。

对lib.*模块选择Android Library,其他模块选择Phone & Tablet Module。

创建一个插件模块,比如app.main:

    修改Application/Library name为App.main
    修改Package name为com.example.mysmall.app.main

如果你不理解,请看我的目录结构
图片描述

上面是模块名称,同时你要注意一下包名:
图片描述

在你新建一个Module的时候as自动命名的应该是:tsou.cn.appchat

因此你在创建的时候要手动修改一下,可能你开始经常不注意直接下一步直接生成了。

进行如下图点击edit 修改一下就可以了

图片描述

  • ***创建bundle.json****

在你的宿主模块下(通常是app)创建bundle.json如下图

图片描述

bundle.json的内容格式如下

{
  "version": "1.0.0",
  "bundles": [
    {
      "uri": "lib.data",
      "pkg": "tsou.cn.lib.data"
    },
    {
      "uri": "lib.utils",
      "pkg": "tsou.cn.lib.utils"
    },
    {
      "uri": "lib.style",
      "pkg": "tsou.cn.lib.style"
    },
    {
      "uri": "lib.layout",
      "pkg": "tsou.cn.lib.layout"
    },
    {
      "uri": "lib.icon",
      "pkg": "tsou.cn.lib.icon"
    },
    {
      "uri": "home",
      "pkg": "tsou.cn.app.home"
    },
    {
      "uri": "chat",
      "pkg": "tsou.cn.app.chat",
      "rules": {
        "FromHomeActivity": "tsou.cn.app.chat.activity.FromHomeActivity"
      }
    },
    {
      "uri": "recom",
      "pkg": "tsou.cn.app.recom"
    },
    {
      "uri": "me",
      "pkg": "tsou.cn.app.me"
    }
  ]
}

特别注意:

  1. 在这里你要注意一下不管是你的插件模块,还是你的依赖模块都需要在这里进行注册,不然你的程序运行会出现错误

    例如:我在这里使用公用的lib.style样式,如果没有注册lib.style那么这个样式编译可以通过,但是运行就会找不到的错误。

  2. 你的宿主(app)moudle不能引入依赖模块(lib),只能是你的插件模块引用你的依赖模块,如下:

    这是我的app宿主:
    图片描述

    这是我的app.chat插件模块:
    图片描述

  3. 列表内容你需要进行编译:

    我这里直接使用的是as来编译:依次为cleanLib->cleanBundle->buildLib->buildBundle
    图片描述

  4. 选择你的app主模块进行运行。

  • **初始化small****
在你的宿主app模块中:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Small.preSetUp(this);
    }
}
  • ***显示主页面**

    主页面主要是一个viewpager来填充各个插件模块的fragment,代码如下:


/**
 * 使用style等lib的时候切记在bundle.json上继续配置,不然会找不到
 * <p>
 * 例如:提示AppTheme找不到让你进行配置问题
 */
public class MainActivity extends AppCompatActivity {

    private ViewPager mMViewPager;
    private TabLayout mToolbarTab;
    /**
     * 图标
     */
    private int[] tabIcons = {
            R.drawable.tab_home,
            R.drawable.tab_weichat,
            R.drawable.tab_recommend,
            R.drawable.tab_user
    };
    private static String[] fragments = new String[]{"home", "chat", "recom", "me"};
    private String[] tab_array;
    private DemandAdapter mDemandAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initView();
        // 给viewpager设置适配器
        setViewPagerAdapter();
        setTabBindViewPager();
        setItem();
    }

    private void initData() {
        tab_array = getResources().getStringArray(R.array.tab_main);
    }

    private void initView() {
        mMViewPager = (ViewPager) findViewById(R.id.mViewPager);
        mToolbarTab = (TabLayout) findViewById(R.id.toolbar_tab);

    }

    private void setViewPagerAdapter() {
        mDemandAdapter = new DemandAdapter(getSupportFragmentManager());
        mMViewPager.setAdapter(mDemandAdapter);
    }

    private void setTabBindViewPager() {
        mToolbarTab.setupWithViewPager(mMViewPager);
    }

    private void setItem() {
        /**
         * 一定要在设置适配器之后设置Icon
         */
        for (int i = 0; i < mToolbarTab.getTabCount(); i++) {
            mToolbarTab.getTabAt(i).setCustomView(getTabView(i));
        }
    }

    public View getTabView(int position) {
        View view = LayoutInflater.from(this).inflate(R.layout.item_tab, null);
        ImageView tab_image = view.findViewById(R.id.tab_image);
        TextView tab_text = view.findViewById(R.id.tab_text);
        tab_image.setImageResource(tabIcons[position]);
        tab_text.setText(tab_array[position]);
        return view;
    }

    /**
     * 适配器
     */
    public class DemandAdapter extends FragmentStatePagerAdapter {

        public DemandAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            /**
             * 在使用createObject前,需要再此页面之前已经存在activity,初始化Small,
             * 否则获取不到Fragment,都是null
             */
            Fragment fragment = Small.createObject("fragment-v4", fragments[position],
                    MainActivity.this);
            return fragment;
        }

        @Override
        public int getCount() {
            return tabIcons.length;
        }

    }
}

特别要注意:

  1. 使用Small.createObject("fragment-v4", fragments[position],
    MainActivity.this);来获取fragment时,各个插件模块的fragment命名一定要是MainFragment不然会出现空指针异常,这是Small框架的问题,在反射调用时已经写死了,同时需要在bundle.json中配置,一般直接创建在跟包名下面:
    图片描述

  2. 在使用createObject前,需要再此页面之前已经存在activity,初始化Small,例如我这里已经有了个启动页面LaunchActivity,不然也会出现空指针
    图片描述

  3. 如果这个时候要运行代码看效果,记得要执行as编译依次为cleanLib->cleanBundle->buildLib->buildBundle,

  4. 记住在每次写完代码,运行到手机的时候都要进行此4部,否则新写的代码无效!

  • **插件之间的数据传递**

一、跳转到Chat模块主页

 在app.chat创建MainActivity,其实插件模块是一个可以独立运行的应用,
 和平时项目中的MainActivity没有区别,可看做独立的应用看待。

切记,在bundle.json中配置,我上面已经配置好了,包括后面要用的FromHomeActivity。
并且运行时别忘了上面的那4部,后面不在说明。
 {
      "uri": "chat",
      "pkg": "tsou.cn.app.chat",
      "rules": {
        "FromHomeActivity": "tsou.cn.app.chat.activity.FromHomeActivity"
      }
    }
执行代码如下:

 Small.setUp(getContext(), new Small.OnCompleteListener() {
                    @Override
                    public void onComplete() {
                        Small.openUri("chat", getContext());
                    }
                });

二、不带参数跳转到Chat模块指定Activity(FromHomeActivity)

执行代码如下:

 Small.setUp(getContext(), new Small.OnCompleteListener() {
                    @Override
                    public void onComplete() {
                        /**
                         * Small.openUri("chat", getContext());
                         * 直接跳转chat模块的主页。
                         *
                         *   Small.openUri("chat/FromHomeActivity", getContext());
                         *   跳转指定页面
                         */
                        Small.openUri("chat/FromHomeActivity", getContext());
                    }
                });

三、带参数跳转到Chat模块指定Activity(FromHomeActivity)


执行代码如下:

 Small.setUp(getContext(), new Small.OnCompleteListener() {
                    @Override
                    public void onComplete() {
                        Small.openUri("chat/FromHomeActivity?name=huangxiaoguo&age=25", getContext());
                    }
                });
FromHomeActivity中接收数据并使用:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_from_home);
        initData();
        initView();
    }

    private void initData() {
        Uri uri = Small.getUri(this);
        if (uri != null) {
            name = uri.getQueryParameter("name");
            age = uri.getQueryParameter("age");
        }
    }

    private void initView() {
        mTextview = (TextView) findViewById(R.id.textview);
        if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(age)) {
            mTextview.setText("name=" + name + ",age=" + age);
//            UIUtils.showToast("name=" + name + ",age=" + age);
        }

四、small支持本地网页组件

small可以直接支持本地网页,我们不用再自己写一个webview页面了!

但是这里的本地网页组件功能很有限,只能显示比较简单的网页,复杂的暂时不支持,
会崩溃,具体你可以试试就知道了。
//Small.openUri("http://www.baidu.com", getContext());

Small.openUri("https://github.com/wequick/Small/issues", getContext());
                break;

五、使用eventBus数据传输

原理不说了,直接上代码
 compile 'org.simple:androideventbus:1.0.5.1'
public interface EvenBusTag {
    String EVENT_GET_DATA = "evevt_get_data";
}

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
            savedInstanceState) {
        EventBus.getDefault().register(this);
        View view = inflater.inflate(R.layout.fragment_home, null);
        initView(view);
        return view;
    }

    @Override
    public void onDestroyView() {
        EventBus.getDefault().unregister(this);
        ThreadUtils.cancelLongBackThread();
        super.onDestroyView();
    }

    @Subscriber(tag = EvenBusTag.EVENT_GET_DATA)
    public void onEvent(String s) {
        UIUtils.showToast(s);
    }
   @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.btn_fack_eventbus:
                EventBus.getDefault().post("eventBus从FromHomeActivity中返回数据了", EvenBusTag.EVENT_GET_DATA);
                finish();
                break;
        }
    }
友好提示:我的这里的所有公用compile引入和一些自己定义的字段都放在lib.data中,
因为我这里只有这个依赖库是专门提供给当前产品使用的,
在开发新的产品时别的依赖库方便直接使用,我个人感觉方便点,当然看你自己想法!

由于篇幅有限插件化更新请看下一篇:android:一步步实现插件化与热更新(二)

点击查看更多内容
13人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
50
获赞与收藏
188

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消