android:一步步实现插件化与热更新
来源:互联网 发布:淘宝宝贝关键词怎么刷 编辑:程序博客网 时间:2024/06/10 10:41
由于插件化开发与热更新最近貌似越来越火,新开的项目准备也使用插件化进行开发!其中遇到不少坑,在这里写了一个小的例子,记录一下开发流程,有助于自己,同时希望能够帮助大家理解,并且对于自身项目接入插件化有所帮助!
插件化
效果:
插件化开发的含义:
插件化开发也是将一个项目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" } ]}
特别注意:
在这里你要注意一下不管是你的插件模块,还是你的依赖模块都需要在这里进行注册,不然你的程序运行会出现错误
例如:我在这里使用公用的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 { @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; } }}
特别要注意:
使用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() { @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中,因为我这里只有这个依赖库是专门提供给当前产品使用的,在开发新的产品时别的依赖库方便直接使用,我个人感觉方便点,当然看你自己想法!
六、插件化更新
修改代码
我修改的是app.chat中FromHomeActivity
在参数传递过来的时候,弹个吐司:UIUtils.showToast("name=" + name + ",age=" + age);
记得修改版本号:
versionCode 2 1——>2 versionName "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; } @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_7android { 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${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); } @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); MultiDex.install(base); TinkerInstaller.install(this); } @Override public 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 = true tinkerOldApkPath = "${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 field tinkerBuildFlavorDirectory = "${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; @Override protected 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() { @Override public 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 { //有权限直接加载apk loadApk(); } } @Override public 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) { //权限申请成功加载apk loadApk(); } 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(); } }}
好了到此插件化与热更新都可以实现了…….
android插件化开发Demo:https://gitee.com/huangxiaoguo/androidChaJianHuaKaiFa
android-Tinker热修复Demo:https://gitee.com/huangxiaoguo/android-TinkerReXiuFu
- android:一步步实现插件化与热更新
- 一步步教你实现Android HotFix热更新
- android插件化,热修复,热更新。
- 增量更新热修复与插件化
- React Native带你一步步实现热更新(CodePush-Android)
- android热插件,热更新,热修复,模块化
- android热插件,热更新,热修复,模块化
- Android插件化与热修复
- Android热更新实现原理
- Android热更新实现原理
- Android热更新实现原理
- Android快速实现热更新
- Android热更新实现原理
- Android快速实现热更新
- android 热更新的实现
- Android快速实现热更新
- Android热更新实现方式
- 使用Tinker与极光推送实现Android热更新
- 如何将Unity游戏构建到iOS设备
- String案例 练习: 将一个字符串进行反转
- 敏捷开发 scrum
- 自定义View实现拖动条SeekBar
- 强化学习入门第四讲 时间差分方法
- android:一步步实现插件化与热更新
- 【云星数据---Scala实战系列(精品版)】:Scala入门教程064-Scala实战源码-类访问伴生对象中的成员2
- python函数应用(1)
- Git基础操作
- phpstorm 远程开发及调试
- 一些好的网站记录(PART 3)
- 'pip' 不是内部或外部命令,也不是可运行的程序或批处理文件。
- 强化学习入门 第五讲 值函数逼近
- 数组和链表的区别