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 = 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"}]
}
特别注意:
在这里你要注意一下不管是你的插件模块,还是你的依赖模块都需要在这里进行注册,不然你的程序运行会出现错误
例如:我在这里使用公用的lib.style样式,如果没有注册lib.style那么这个样式编译可以通过,但是运行就会找不到的错误。
你的宿主(app)moudle不能引入依赖模块(lib),只能是你的插件模块引用你的依赖模块,如下:
这是我的app宿主:
这是我的app.chat插件模块:
列表内容你需要进行编译:
我这里直接使用的是as来编译:依次为cleanLib->cleanBundle->buildLib->buildBundle
选择你的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;}}
}
特别要注意:
使用Small.createObject(“fragment-v4”, fragments[position],
MainActivity.this);来获取fragment时,各个插件模块的fragment命名一定要是MainFragment不然会出现空指针异常,这是Small框架的问题,在反射调用时已经写死了,同时需要在bundle.json中配置,一般直接创建在跟包名下面:在使用createObject前,需要再此页面之前已经存在activity,初始化Small,例如我这里已经有了个启动页面LaunchActivity,不然也会出现空指针
如果这个时候要运行代码看效果,记得要执行as编译依次为cleanLib->cleanBundle->buildLib->buildBundle,、
记住在每次写完代码,运行到手机的时候都要进行此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中,因为我这里只有这个依赖库是专门提供给当前产品使用的,在开发新的产品时别的依赖库方便直接使用,我个人感觉方便点,当然看你自己想法!
六、插件化更新
修改代码
我修改的是app.chat中FromHomeActivity
在参数传递过来的时候,弹个吐司:UIUtils.showToast("name=" + name + ",age=" + age);
记得修改版本号:
versionCode 2 1——>2versionName "1.1" 1.0->1.1注意你修改的哪个插件修改哪个插件的版本即可,不是宿主app模块,我这里修改的是app.chat
两步:buildLib->buildBundle
查找so,在你的宿主模块中
部署服务器,我直接在我的电脑上部署一个tomcat
服务器的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"}] }
实现插件化更新
/*** 插件化更新*/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一步步实现插件化与热更新相关推荐
- login组件的两种用法_Android-模块化、组件化、插件化、热修复-组件化-组件间的通信(本地,下沉,bus,路由)...
延续上一篇 MonkeyLei:Android-模块化.组件化.插件化.热修复-组件化工程构建+页面路由多种方式实践 ,我们进行搞下组件之间的通信.比如登录成功后怎么通知其他页面刷新: 方式可能有很多 ...
- 插件化、热补丁中绕不开的Proguard的坑
文章主体部分已经发表于<程序员>杂志2018年2月期,内容略有改动. ProGuard简介 ProGuard是2002年由比利时程序员Eric Lafortune发布的一款优秀的开源代码优 ...
- 动态加载、插件化、热部署、热修复(更新)知识汇总
开发中经常能听到动态加载,插件化,热部署等词,动态加载到底是何方神物,它能实现什么功能,实现原理又如何?动态加载和插件化.热部署又有着什么样的联系呢?下面我们一起来学习吧. 1. 基本知识 1.1 动 ...
- 安卓插件化与热修复的选型
参考文章: 安卓插件化的过去现在和未来 张涛 http://kymjs.com/code/2016/05/04/01 安卓插件化从入门到放弃 包建强 http://www.infoq.com/cn/n ...
- [Android]用架构师角度看插件化(1)-Replugin入门剖析
多谢一直以来的支持,组件化的内容,应该会有一段时间不再更新,一些非常关键的技术将会在我将要出版的组件化书籍中提及. 组件化模块化的开发适合于中小型企业的业务叠加,和代码重用.而插件化的开发将组件化和模 ...
- [Android]用架构师角度看插件化(2)-Replugin 唯一hook点
Replugin,为何我选择要研究这个的插件呢?很大的原因是因为它的介绍中说明,他只会有一个hook点. 一.Hook hook点是什么? 我们入门Android的时候,一定会看到过这个图,但是你确定 ...
- [Android]用架构师角度看插件化(3)-Replugin 需要占坑跳转?
一.占坑 什么是占坑?为什么要占坑? Android插件化中,从一个插件Activity跳转到不同插件的Activity的时候,是否可以能正常跳转成功? 声明Activity需要配置什么? 声明Act ...
- Android动态替换dex,Android DexClassLoader动态加载与插件化开发
参考链接: 一. 基本概念和注意点 1.1 首先需要了解一点:在Android中可以动态加载,但无法像Java中那样方便动态加载jar 原因:Android的虚拟机(Dalvik VM)是不认识Jav ...
- Android面试之百题经典Android答案——cookie,session,JNI,AIDL,Binder,ClassLoader,AMS,WMS,PWS,热更新,插件化,Hook,dex
一.Activity生命周期 实际面试中可能会以实例形式出现,比如:启动A,再从A启动B,请描述各生命周期 二.Activity的启动模式 Activity的启动模式有4种,分别是Standard.S ...
最新文章
- [转载]ESFramework 4.0 快速上手(15) -- 客户端登录验证
- 深入云原生 AI:基于 Alluxio 数据缓存的大规模深度学习训练性能优化
- 绝对定位和浮动的区别和运用
- [iOS]利用Appicon and Launchimage Maker生成并配置iOSApp的图标和启动页
- android fragment面试,Android fragment之间传递数据的方式?
- 表格存储(TableStore)
- 网络设置管理 NetSetMan Pro v4.7.1 Lite 绿色便携版
- JS对象定义和基本方法
- Mapper和dao
- PHP获取中国所有的大学,全国300所大学的BBS论坛.doc
- CRM客户关系管理分析模型——RFM模型
- 连接多个内网的方法,想连多少个连多少个
- skyline三维地图与arcgis二维地图联动
- 开发人员面试62到经典题
- seamless kernel updates
- 计算机科学与技术影视,影视作品可视化研究-计算机科学与技术专业论文.docx
- java数组排序sort原理,ZooKeeper的十二连问
- 触摸操作(单手旋转双手缩放)
- 游戏行业回暖,但距离春天还有一段距离
- 基于java社区疫情防控系统计算机毕业设计源码+系统+lw文档+mysql数据库+调试部署
热门文章
- JQ input框单多图上传
- SQL Server 数据库之常量
- bss是什么_BSS的完整形式是什么?
- 推荐系统实践(五)----基于图的推荐算法
- python找最长的单词_318. 最长单词长度乘积(Python)
- 基于机器学习的古代汉语切分标注算法及语料库研究(毕业设计包含完整代码+论文+资料ppt)
- Educode--头歌 《软件工程》实验作业2
- 树莓派linux led字符设备驱动(原子操作)
- 台式计算机硬盘的安装位置,台式主机扩大存储,7步教你完美安装机械硬盘
- 北京现代2015款途胜中控系统、多媒体系统、车机系统升级教程