1. 程式人生 > >android:使用small一步步實現外掛化與熱更新

android:使用small一步步實現外掛化與熱更新

由於外掛化開發與熱更新最近貌似越來越火,新開的專案準備也使用外掛化進行開發!其中遇到不少坑,在這裡寫了一個小的例子,記錄一下開發流程,有助於自己,同時希望能夠幫助大家理解,並且對於自身專案接入外掛化有所幫助!

外掛化

效果:

這裡寫圖片描述

外掛化開發的含義:

外掛化開發也是將一個專案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模組中:

package tsou.cn.mysmalltest;

import android.app.Application;

import net.wequick.small.Small;

/**
 * Created by Administrator on 2017/11/27 0027.
 */

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

    主頁面主要是一個viewpager來填充各個外掛模組的fragment,程式碼如下:

package tsou.cn.mysmalltest;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import net.wequick.small.Small;

/**
 * 使用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());
                    }
                });
注意:Small.setUp是驗證外掛是否載入完成, Small.openUri是執行跳轉
      如果確定外掛已經載入過了,或者這當前module內部進行跳轉可以直接使用 Small.openUri

二、不帶引數跳轉到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中,因為我這裡只有這個依賴庫是專門提供給當前產品使用的,在開發新的產品時別的依賴庫方便直接使用,我個人感覺方便點,當然看你自己想法!

六、外掛化更新

  1. 修改程式碼

    我修改的是app.chat中FromHomeActivity

    在引數傳遞過來的時候,彈個吐司:
    
    UIUtils.showToast("name=" + name + ",age=" + age);
  2. 記得修改版本號:

     versionCode 2   1——>2
      versionName "1.1"  1.0->1.1
    
    注意你修改的哪個外掛修改哪個外掛的版本即可,不是宿主app模組,我這裡修改的是app.chat
  3. 兩步:buildLib->buildBundle

  4. 查詢so,在你的宿主模組中

    這裡寫圖片描述

  5. 部署伺服器,我直接在我的電腦上部署一個tomcat

    這裡寫圖片描述

  6. 伺服器的bundle.json

    {
      "manifest": {
        "bundles": [
          {
            "pkg": "tsou.cn.lib.data",
            "uri": "lib.data"
          },
          {
            "pkg": "tsou.cn.lib.utils",
            "uri": "lib.utils"
          },
          {
            "pkg": "tsou.cn.lib.style",
            "uri": "lib.style"
          },
          {
            "pkg": "tsou.cn.lib.layout",
            "uri": "lib.layout"
          },
          {
            "pkg": "tsou.cn.lib.icon",
            "uri": "lib.icon"
          },
          {
            "pkg": "tsou.cn.app.home",
            "uri": "home"
          },
          {
            "pkg": "tsou.cn.app.chat",
            "rules": {
              "FromHomeActivity": "tsou.cn.app.chat.activity.FromHomeActivity"
            },
            "uri": "chat"
          },
          {
            "pkg": "tsou.cn.app.recom",
            "uri": "recom"
          },
          {
            "pkg": "tsou.cn.app.me",
            "uri": "me"
          }
        ],
        "version": "1.0.0"
      },
      "updates": [
        {
          "pkg": "tsou.cn.app.chat",
          "url": "http://192.168.19.125:8080/json/libtsou_cn_app_chat.so"
        }
      ]
    }
  7. 實現外掛化更新


    /**
     * 外掛化更新
     */
    private void checkUpgrade() {
        new UpgradeManager(getContext()).checkUpgrade();
    }

    private static class UpgradeManager {

        private static class UpdateInfo {
            public String packageName;
            public String downloadUrl;
        }

        private static class UpgradeInfo {
            public JSONObject manifest;
            public List<UpdateInfo> updates;
        }

        private interface OnResponseListener {
            void onResponse(UpgradeInfo info);
        }

        private interface OnUpgradeListener {
            void onUpgrade(boolean succeed);
        }

        private static class ResponseHandler extends Handler {
            private OnResponseListener mListener;

            public ResponseHandler(OnResponseListener listener) {
                mListener = listener;
            }

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 1:
                        mListener.onResponse((UpgradeInfo) msg.obj);
                        break;
                }
            }
        }

        private ResponseHandler mResponseHandler;

        private Context mContext;
        private ProgressDialog mProgressDlg;

        public UpgradeManager(Context context) {
            mContext = context;
        }

        public void checkUpgrade() {
            mProgressDlg = ProgressDialog.show(mContext, "Small", "檢查更新...", false, true);
            requestUpgradeInfo(Small.getBundleVersions(), new OnResponseListener() {
                @Override
                public void onResponse(UpgradeInfo info) {
                    mProgressDlg.setMessage("升級中...");
                    upgradeBundles(info,
                            new OnUpgradeListener() {
                                @Override
                                public void onUpgrade(boolean succeed) {
                                    mProgressDlg.dismiss();
                                    mProgressDlg = null;
                                    String text = succeed ?
                                            "升級成功!切換到後臺並返回到前臺來檢視更改"
                                            : "升級失敗!";
                                    UIUtils.showToast(text);
                                }
                            });
                }
            });
        }

        /**
         * @param versions
         * @param listener
         */
        private void requestUpgradeInfo(Map versions, OnResponseListener listener) {
            System.out.println(versions); // this should be passed as HTTP parameters
            mResponseHandler = new ResponseHandler(listener);
            ThreadUtils.runOnLongBackThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // Example HTTP request to get the upgrade bundles information.
                        // Json format see http://wequick.github.io/small/upgrade/bundles.json
                        URL url = new URL("http://192.168.19.125:8080/json/bundle.json");
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        StringBuilder sb = new StringBuilder();
                        InputStream is = conn.getInputStream();
                        byte[] buffer = new byte[1024];
                        int length;
                        while ((length = is.read(buffer)) != -1) {
                            sb.append(new String(buffer, 0, length));
                        }

                        // Parse json
                        JSONObject jo = new JSONObject(sb.toString());
                        JSONObject mf = jo.has("manifest") ? jo.getJSONObject("manifest") : null;
                        JSONArray updates = jo.getJSONArray("updates");
                        int N = updates.length();
                        List<UpdateInfo> infos = new ArrayList<UpdateInfo>(N);
                        for (int i = 0; i < N; i++) {
                            JSONObject o = updates.getJSONObject(i);
                            UpdateInfo info = new UpdateInfo();
                            info.packageName = o.getString("pkg");
                            info.downloadUrl = o.getString("url");
                            infos.add(info);
                        }

                        // Post message
                        UpgradeInfo ui = new UpgradeInfo();
                        ui.manifest = mf;
                        ui.updates = infos;
                        Message.obtain(mResponseHandler, 1, ui).sendToTarget();
                    } catch (Exception e) {
                        e.printStackTrace();
                        ThreadUtils.runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                mProgressDlg.dismiss();
                                mProgressDlg = null;
                                UIUtils.showToast("更新失敗");
                            }
                        });
                    }
                }
            });
        }

        private static class DownloadHandler extends Handler {
            private OnUpgradeListener mListener;

            public DownloadHandler(OnUpgradeListener listener) {
                mListener = listener;
            }

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 1:
                        mListener.onUpgrade((Boolean) msg.obj);
                        break;
                }
            }
        }

        private DownloadHandler mHandler;

        private void upgradeBundles(final UpgradeInfo info,
                                    final OnUpgradeListener listener) {
            // Just for example, you can do this by OkHttp or something.
            mHandler = new DownloadHandler(listener);
            ThreadUtils.runOnLongBackThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // Update manifest
                        if (info.manifest != null) {
                            if (!Small.updateManifest(info.manifest, false)) {

                                Message.obtain(mHandler, 1, false).sendToTarget();
                                return;
                            }
                        }
                        // Download bundles
                        List<UpdateInfo> updates = info.updates;
                        for (UpdateInfo u : updates) {
                            // Get the patch file for downloading
                            net.wequick.small.Bundle bundle = Small.getBundle(u.packageName);
                            File file = bundle.getPatchFile();

                            // Download
                            URL url = new URL(u.downloadUrl);
                            HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
                            InputStream is = urlConn.getInputStream();
                            OutputStream os = new FileOutputStream(file);
                            byte[] buffer = new byte[1024];
                            int length;
                            while ((length = is.read(buffer)) != -1) {
                                os.write(buffer, 0, length);
                            }
                            os.flush();
                            os.close();
                            is.close();

                            // Upgrade
                            bundle.upgrade();
                        }
                        Message.obtain(mHandler, 1, true).sendToTarget();
                    } catch (IOException e) {
                        e.printStackTrace();
                        Message.obtain(mHandler, 1, false).sendToTarget();
                        ThreadUtils.runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                mProgressDlg.dismiss();
                                mProgressDlg = null;
                                UIUtils.showToast("更新失敗");
                            }
                        });
                    }
                }
            });
        }
    }

到此你就可以使用small進行外掛化開發了……..

熱更新

熱更新其實來源於外掛化,如果你使用了上面的Small進行外掛化開發,就可以直接進行外掛化更新了!如果你沒有使用外掛化開發,你就可以只有熱更新實現程式碼的熱修復。

這裡主要使用的是騰訊的Tinker。

  • 在peoject的build中配置如下:
  dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
TINKER_VERSION需要在gradle.properties中進行配置
TINKER_VERSION=1.7.7
  • 接下來是配置app的build
這是我的,可以直接使用:

apply plugin: 'com.android.application'
//配置java版本
def javaVersion = JavaVersion.VERSION_1_7

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.2"
    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }
    //recommend
    dexOptions {
        jumboMode = true
    }
    signingConfigs {
        release {
            keyAlias 'tsou'
            keyPassword 'tsou123'
            storeFile file('D:/study/TinkerTest/app/keystore/tinkertest.jks')
            storePassword 'tsou123'
        }
        debug {
            keyAlias 'tsou'
            keyPassword 'tsou123'
            storeFile file('D:/study/TinkerTest/app/keystore/tinkertest.jks')
            storePassword 'tsou123'
        }
    }
    defaultConfig {
        applicationId "tsou.cn.tinkertest"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
        //客戶端版本更新補丁
        buildConfigField "String", "TINKER_ID", "\"2.0\""
        buildConfigField "String", "PLATFORM", "\"all\""
    }

    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    //可選,用於生成application類
    compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    //tinker的核心庫
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compile 'com.android.support:multidex:1.0.1'
}
def bakPath = file("${buildDir}/bakApk/")

//老版本的檔案所在的位置,大家也可以動態配置,不用每次都在這裡修改
ext {
    tinkerEnabled = true

    tinkerOldApkPath = "${bakPath}/app-release-1110-16-24-35.apk"
    tinkerApplyMappingPath = "${bakPath}/app-release-1110-16-24-35-mapping.txt"
    tinkerApplyResourcePath = "${bakPath}/app-release-1110-16-24-35-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}
/**
 * tinkerId一定要有
 */
if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'
    tinkerPatch {
        oldApk = getOldApkPath()
        ignoreWarning = true
        useSign = true
        tinkerEnable = buildWithTinker()
        buildConfig {
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()
            tinkerId = "1.0"/*getTinkerIdValue()*/
            keepDexApply = false
        }
        dex {
            dexMode = "jar"
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }
        lib {
            pattern = ["lib/*/*.so"]
        }
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            ignoreChange = ["assets/sample_meta.txt"]
            largeModSize = 100
        }
        packageConfig {
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            configField("patchVersion", "1.0")
        }

        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
//        path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    android.applicationVariants.all { variant ->
        def taskName = variant.name
        def date = new Date().format("MMdd-HH-mm-ss")
        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

                    }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${fl