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

插件化

效果:

插件化开发的含义:

插件化开发也是将一个项目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 = falseandroid {compileSdkVersion = 26buildToolsVersion = "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 {@Overridepublic 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;@Overrideprotected 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);}@Overridepublic Fragment getItem(int position) {/*** 在使用createObject前,需要再此页面之前已经存在activity,初始化Small,* 否则获取不到Fragment,都是null*/Fragment fragment = Small.createObject("fragment-v4", fragments[position],MainActivity.this);return fragment;}@Overridepublic 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() {@Overridepublic void onComplete() {Small.openUri("chat", getContext());}});
注意:Small.setUp是验证插件是否加载完成, Small.openUri是执行跳转如果确定插件已经加载过了,或者这当前module内部进行跳转可以直接使用 Small.openUri

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

执行代码如下:Small.setUp(getContext(), new Small.OnCompleteListener() {@Overridepublic 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() {@Overridepublic void onComplete() {Small.openUri("chat/FromHomeActivity?name=huangxiaoguo&age=25", getContext());}});
FromHomeActivity中接收数据并使用:@Overrideprotected 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";
}
@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, BundlesavedInstanceState) {EventBus.getDefault().register(this);View view = inflater.inflate(R.layout.fragment_home, null);initView(view);return view;}@Overridepublic void onDestroyView() {EventBus.getDefault().unregister(this);ThreadUtils.cancelLongBackThread();super.onDestroyView();}@Subscriber(tag = EvenBusTag.EVENT_GET_DATA)public void onEvent(String s) {UIUtils.showToast(s);}
   @Overridepublic 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——>2versionName "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;}@Overridepublic 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() {@Overridepublic void onResponse(UpgradeInfo info) {mProgressDlg.setMessage("升级中...");upgradeBundles(info,new OnUpgradeListener() {@Overridepublic 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 parametersmResponseHandler = new ResponseHandler(listener);ThreadUtils.runOnLongBackThread(new Runnable() {@Overridepublic void run() {try {// Example HTTP request to get the upgrade bundles information.// Json format see http://wequick.github.io/small/upgrade/bundles.jsonURL 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 jsonJSONObject 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 messageUpgradeInfo ui = new UpgradeInfo();ui.manifest = mf;ui.updates = infos;Message.obtain(mResponseHandler, 1, ui).sendToTarget();} catch (Exception e) {e.printStackTrace();ThreadUtils.runOnUiThread(new Runnable() {@Overridepublic void run() {mProgressDlg.dismiss();mProgressDlg = null;UIUtils.showToast("更新失败");}});}}});}private static class DownloadHandler extends Handler {private OnUpgradeListener mListener;public DownloadHandler(OnUpgradeListener listener) {mListener = listener;}@Overridepublic 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() {@Overridepublic void run() {try {// Update manifestif (info.manifest != null) {if (!Small.updateManifest(info.manifest, false)) {Message.obtain(mHandler, 1, false).sendToTarget();return;}}// Download bundlesList<UpdateInfo> updates = info.updates;for (UpdateInfo u : updates) {// Get the patch file for downloadingnet.wequick.small.Bundle bundle = Small.getBundle(u.packageName);File file = bundle.getPatchFile();// DownloadURL 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();// Upgradebundle.upgrade();}Message.obtain(mHandler, 1, true).sendToTarget();} catch (IOException e) {e.printStackTrace();Message.obtain(mHandler, 1, false).sendToTarget();ThreadUtils.runOnUiThread(new Runnable() {@Overridepublic 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_7android {compileSdkVersion 26buildToolsVersion "26.0.2"compileOptions {sourceCompatibility javaVersiontargetCompatibility javaVersion}//recommenddexOptions {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 15targetSdkVersion 26versionCode 1versionName "1.0"testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"multiDexEnabled truebuildConfigField "String", "MESSAGE", "\"I am the base apk\""//客户端版本更新补丁buildConfigField "String", "TINKER_ID", "\"2.0\""buildConfigField "String", "PLATFORM", "\"all\""}buildTypes {release {minifyEnabled truesigningConfig signingConfigs.releaseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}debug {debuggable trueminifyEnabled falsesigningConfig signingConfigs.debugproguardFiles 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 = truetinkerOldApkPath = "${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 fieldtinkerBuildFlavorDirectory = "${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 = trueuseSign = truetinkerEnable = 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() > 0android.applicationVariants.all { variant ->def taskName = variant.namedef 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}") : bakPathfrom variant.outputs.outputFileinto destPathrename { String fileName ->fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")}from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"into destPathrename { String fileName ->fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")}from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"into destPathrename { 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 tinkerTaskdef 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 tinkerTaskdef preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")preAssembleTask.doFirst {String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"}}}}}
}
注意 修改buildConfigField
//客户端版本更新补丁
buildConfigField "String", "TINKER_ID", "\"2.0\""如果更新客户端补丁需要修改,例如我把1.0改为2.0
  • 配置application
package tsou.cn.tinkertest;import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.support.multidex.MultiDex;import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.app.ApplicationLike;
import com.tencent.tinker.loader.shareutil.ShareConstants;/*** Created by Administrator on 2017/10/27 0027.*/@DefaultLifeCycle(application = ".SimpleTinkerInApplication",flags = ShareConstants.TINKER_ENABLE_ALL,loadVerifyFlag =true)
public class SimpleTinkerInApplicationLike extends ApplicationLike {public SimpleTinkerInApplicationLike (Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);}@Overridepublic void onBaseContextAttached(Context base) {super.onBaseContextAttached(base);MultiDex.install(base);TinkerInstaller.install(this);}@Overridepublic void onCreate() {super.onCreate();}
}
SimpleTinkerInApplicationLike 并不是一个application
真正的application是@DefaultLifeCycle(application = “.SimpleTinkerInApplication”,这个
所以在androidmanifest中配置applica的name
 <application
        android:name=".SimpleTinkerInApplication"android:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  • 运行你的项目使用release版本

效果如下:

  • 更改你的build.gradle
//老版本的文件所在的位置,大家也可以动态配置,不用每次都在这里修改
ext {tinkerEnabled = truetinkerOldApkPath = "${bakPath}/app-release-1201-13-55-26.apk"tinkerApplyMappingPath = "${bakPath}/app-release-1201-13-55-26-mapping.txt"tinkerApplyResourcePath = "${bakPath}/app-release-1201-13-55-26-R.txt"//only use for build all flavor, if not, just ignore this fieldtinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
  • 更改代码:把“我的”改为“2222”

  • 然后点击 编译

  • 生成目录如下

  • 把patch_signed_7zip.apk放到手机根目录下,再执行更新

  • 热更新代码如下:
package tsou.cn.tinkertest;import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;import com.tencent.tinker.lib.tinker.TinkerInstaller;import java.io.File;public class MainActivity extends AppCompatActivity {private TextView tv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tv = (TextView) findViewById(R.id.tv);Button loadPath = (Button) findViewById(R.id.loadPath);tv.setText("2222");loadPath.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {requestPermi();}});}private void requestPermi() {if (ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.READ_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {// Should we show an explanation?if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,Manifest.permission.READ_EXTERNAL_STORAGE)) {} else {ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},100);}} else {//有权限直接加载apkloadApk();}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);switch (requestCode) {case 100: {// If request is cancelled, the result arrays are empty.if (grantResults.length > 0&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {//权限申请成功加载apkloadApk();} else {}return;}}}public void loadApk() {String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk";File file = new File(path);if (file.exists()) {Toast.makeText(this, "补丁已经存在", Toast.LENGTH_SHORT).show();TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);} else {Toast.makeText(this, "补丁已经不存在", Toast.LENGTH_SHORT).show();}}
}

打包

切换到Project目录树,右键跟目录,新建sign目录,添加release.jks签名文件。

在app模块的build.gradle中增加签名配置(密码改成自己的):

signingConfigs {release {storeFile file('../sign/release.jks')storePassword "small"keyAlias "small"keyPassword "small"}
}
buildTypes {release {signingConfig signingConfigs.release}
}

选择Build Variant

cleanLib->cleanBundle->buildLib->buildBundle ;run运行后 得到正式包


好了到此插件化与热更新都可以实现了…….

android插件化开发Demo:https://gitee.com/huangxiaoguo/androidChaJianHuaKaiFa

android-Tinker热修复Demo:https://gitee.com/huangxiaoguo/android-TinkerReXiuFu

android:使用small一步步实现插件化与热更新相关推荐

  1. login组件的两种用法_Android-模块化、组件化、插件化、热修复-组件化-组件间的通信(本地,下沉,bus,路由)...

    延续上一篇 MonkeyLei:Android-模块化.组件化.插件化.热修复-组件化工程构建+页面路由多种方式实践 ,我们进行搞下组件之间的通信.比如登录成功后怎么通知其他页面刷新: 方式可能有很多 ...

  2. 插件化、热补丁中绕不开的Proguard的坑

    文章主体部分已经发表于<程序员>杂志2018年2月期,内容略有改动. ProGuard简介 ProGuard是2002年由比利时程序员Eric Lafortune发布的一款优秀的开源代码优 ...

  3. 动态加载、插件化、热部署、热修复(更新)知识汇总

    开发中经常能听到动态加载,插件化,热部署等词,动态加载到底是何方神物,它能实现什么功能,实现原理又如何?动态加载和插件化.热部署又有着什么样的联系呢?下面我们一起来学习吧. 1. 基本知识 1.1 动 ...

  4. 安卓插件化与热修复的选型

    参考文章: 安卓插件化的过去现在和未来 张涛 http://kymjs.com/code/2016/05/04/01 安卓插件化从入门到放弃 包建强 http://www.infoq.com/cn/n ...

  5. [Android]用架构师角度看插件化(1)-Replugin入门剖析

    多谢一直以来的支持,组件化的内容,应该会有一段时间不再更新,一些非常关键的技术将会在我将要出版的组件化书籍中提及. 组件化模块化的开发适合于中小型企业的业务叠加,和代码重用.而插件化的开发将组件化和模 ...

  6. [Android]用架构师角度看插件化(2)-Replugin 唯一hook点

    Replugin,为何我选择要研究这个的插件呢?很大的原因是因为它的介绍中说明,他只会有一个hook点. 一.Hook hook点是什么? 我们入门Android的时候,一定会看到过这个图,但是你确定 ...

  7. [Android]用架构师角度看插件化(3)-Replugin 需要占坑跳转?

    一.占坑 什么是占坑?为什么要占坑? Android插件化中,从一个插件Activity跳转到不同插件的Activity的时候,是否可以能正常跳转成功? 声明Activity需要配置什么? 声明Act ...

  8. Android动态替换dex,Android DexClassLoader动态加载与插件化开发

    参考链接: 一. 基本概念和注意点 1.1 首先需要了解一点:在Android中可以动态加载,但无法像Java中那样方便动态加载jar 原因:Android的虚拟机(Dalvik VM)是不认识Jav ...

  9. Android面试之百题经典Android答案——cookie,session,JNI,AIDL,Binder,ClassLoader,AMS,WMS,PWS,热更新,插件化,Hook,dex

    一.Activity生命周期 实际面试中可能会以实例形式出现,比如:启动A,再从A启动B,请描述各生命周期 二.Activity的启动模式 Activity的启动模式有4种,分别是Standard.S ...

最新文章

  1. [转载]ESFramework 4.0 快速上手(15) -- 客户端登录验证
  2. 深入云原生 AI:基于 Alluxio 数据缓存的大规模深度学习训练性能优化
  3. 绝对定位和浮动的区别和运用
  4. [iOS]利用Appicon and Launchimage Maker生成并配置iOSApp的图标和启动页
  5. android fragment面试,Android fragment之间传递数据的方式?
  6. 表格存储(TableStore)
  7. 网络设置管理 NetSetMan Pro v4.7.1 Lite 绿色便携版
  8. JS对象定义和基本方法
  9. Mapper和dao
  10. PHP获取中国所有的大学,全国300所大学的BBS论坛.doc
  11. CRM客户关系管理分析模型——RFM模型
  12. 连接多个内网的方法,想连多少个连多少个
  13. skyline三维地图与arcgis二维地图联动
  14. 开发人员面试62到经典题
  15. seamless kernel updates
  16. 计算机科学与技术影视,影视作品可视化研究-计算机科学与技术专业论文.docx
  17. java数组排序sort原理,ZooKeeper的十二连问
  18. 触摸操作(单手旋转双手缩放)
  19. 游戏行业回暖,但距离春天还有一段距离
  20. 基于java社区疫情防控系统计算机毕业设计源码+系统+lw文档+mysql数据库+调试部署

热门文章

  1. JQ input框单多图上传
  2. SQL Server 数据库之常量
  3. bss是什么_BSS的完整形式是什么?
  4. 推荐系统实践(五)----基于图的推荐算法
  5. python找最长的单词_318. 最长单词长度乘积(Python)
  6. 基于机器学习的古代汉语切分标注算法及语料库研究(毕业设计包含完整代码+论文+资料ppt)
  7. Educode--头歌 《软件工程》实验作业2
  8. 树莓派linux led字符设备驱动(原子操作)
  9. 台式计算机硬盘的安装位置,台式主机扩大存储,7步教你完美安装机械硬盘
  10. 北京现代2015款途胜中控系统、多媒体系统、车机系统升级教程