Android微信Tinker热更新详细使用

作者:Gimi丶 时间:2021-08-24 01:58:47 

先看一下效果图

Android微信Tinker热更新详细使用

Tinker已知问题

由于原理与系统限制,Tinker有以下已知问题:

  • Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;

  • 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;

  • 在Android N上,补丁对应用启动时间有轻微的影响;

  • 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;

  • 由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新;

  • 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

1.首先在项目的build中,集成tinker插件 ,如下所示(目前最新版是1.7.6)

先看结构图,只有几个类而已:

Android微信Tinker热更新详细使用

项目中的build集成


buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.6')
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

1.再将app的build中的关联属性添加进去,这些属性都是经过测试过的,都有注释显示,如果自己需要其他属性,可以自己去github上查看并集成,文章末尾会送上地址,ps:官方的集成特别麻烦,有时候一整天都有可能搞不定,根据自己的需求和情况来添加,末尾会送上demo


apply plugin: 'com.android.application'

def javaVersion = JavaVersion.VERSION_1_7
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"

compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//recommend
dexOptions {
jumboMode = true
}

defaultConfig {
applicationId "com.tinker.demo.tinkerdemo"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

buildConfigField "String", "MESSAGE", "\"I am the base apk\""

buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""
}

signingConfigs {
release {
 try {
 storeFile file("./keystore/release.keystore")
 storePassword "testres"
 keyAlias "testres"
 keyPassword "testres"
 } catch (ex) {
 throw new InvalidUserDataException(ex.toString())
 }
}

debug {
 storeFile file("./keystore/debug.keystore")
}
}

buildTypes {
release {
 minifyEnabled true
 signingConfig signingConfigs.release
 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
 debuggable true
 minifyEnabled false
 signingConfig signingConfigs.debug
}
}

sourceSets {
main {
 jniLibs.srcDirs = ['libs']
}
}

}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
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:23.1.1"
testCompile 'junit:junit:4.12'

compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compile "com.android.support:multidex:1.0.1"
}

def gitSha() {
try {
// String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
String gitRev = "1008611"
if (gitRev == null) {
 throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}

def bakPath = file("${buildDir}/bakApk/")

ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true

//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-0113-14-01-29.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0113-14-01-29-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 getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}

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

def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'

tinkerPatch {
/**
 * 默认为null
 * 将旧的apk和新的apk建立关联
 * 从build / bakApk添加apk
 */
oldApk = getOldApkPath()
/**
 * 可选,默认'false'
 *有些情况下我们可能会收到一些警告
 *如果ignoreWarning为true,我们只是断言补丁过程
 * case 1:minSdkVersion低于14,但是你使用dexMode与raw。
 * case 2:在AndroidManifest.xml中新添加Android组件,
 * case 3:装载器类在dex.loader {}不保留在主要的dex,
 * 它必须让tinker不工作。
 * case 4:在dex.loader {}中的loader类改变,
 * 加载器类是加载补丁dex。改变它们是没有用的。
 * 它不会崩溃,但这些更改不会影响。你可以忽略它
 * case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建
 */
ignoreWarning = false

/**
 *可选,默认为“true”
 * 是否签名补丁文件
 * 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功
 * 我们将使用sign配置与您的构建类型
 */
useSign = true

/**
 可选,默认为“true”
 是否使用tinker构建
 */
tinkerEnable = buildWithTinker()

/**
 * 警告,applyMapping会影响正常的android build!
 */
buildConfig {
 /**
 *可选,默认为'null'
 * 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的
 * apk映射文件如果minifyEnabled是启用!
 * 警告:你必须小心,它会影响正常的组装构建!
 */
 applyMapping = getApplyMappingPath()
 /**
 *可选,默认为'null'
 * 很高兴保持资源ID从R.txt文件,以减少java更改
 */
 applyResourceMapping = getApplyResourceMappingPath()

/**
 *必需,默认'null'
 * 因为我们不想检查基地apk与md5在运行时(它是慢)
 * tinkerId用于在试图应用补丁时标识唯一的基本apk。
 * 我们可以使用git rev,svn rev或者简单的versionCode。
 * 我们将在您的清单中自动生成tinkerId
 */
 tinkerId = getTinkerIdValue()

/**
 *如果keepDexApply为true,则表示dex指向旧apk的类。
 * 打开这可以减少dex diff文件大小。
 */
 keepDexApply = false
}

dex {
 /**
 *可选,默认'jar'
 * 只能是'raw'或'jar'。对于原始,我们将保持其原始格式
 * 对于jar,我们将使用zip格式重新包装dexes。
 * 如果你想支持下面14,你必须使用jar
 * 或者你想保存rom或检查更快,你也可以使用原始模式
 */
 dexMode = "jar"

/**
 *必需,默认'[]'
 * apk中的dexes应该处理tinkerPatch
 * 它支持*或?模式。
 */
 pattern = ["classes*.dex",
  "assets/secondary-dex-?.jar"]
 /**
 *必需,默认'[]'
 * 警告,这是非常非常重要的,加载类不能随补丁改变。
 * 因此,它们将从补丁程序中删除。
 * 你必须把下面的类放到主要的dex。
 * 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}
 * 自己的tinkerLoader,和你使用的类
 *
 */
 loader = [
  //use sample, let BaseBuildInfo unchangeable with tinker
  "tinker.sample.android.app.BaseBuildInfo"
 ]
}

lib {
 /**
 可选,默认'[]'
 apk中的图书馆应该处理tinkerPatch
 它支持*或?模式。
 对于资源库,我们只是在补丁目录中恢复它们
 你可以得到他们在TinkerLoadResult与Tinker
 */
 pattern = ["lib/armeabi/*.so"]
}

res {
 /**
 *可选,默认'[]'
 * apk中的什么资源应该处理tinkerPatch
 * 它支持*或?模式。
 * 你必须包括你在这里的所有资源,
 * 否则,他们不会重新包装在新的apk资源。
 */
 pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

/**
 *可选,默认'[]'
 *资源文件排除模式,忽略添加,删除或修改资源更改
 * *它支持*或?模式。
 * *警告,我们只能使用文件没有relative与resources.arsc
 */
 ignoreChange = ["assets/sample_meta.txt"]

/**
 *默认100kb
 * *对于修改资源,如果它大于'largeModSize'
 * *我们想使用bsdiff算法来减少补丁文件的大小
 */
 largeModSize = 100
}

packageConfig {
 /**
 *可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
 * 包元文件gen。路径是修补程序文件中的assets / package_meta.txt
 * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
 * 或TinkerLoadResult.getPackageConfigByName
 * 我们将从旧的apk清单为您自动获取TINKER_ID,
 * 其他配置文件(如下面的patchMessage)不是必需的
 */
 configField("patchMessage", "tinker is sample to use")
 /**
 *只是一个例子,你可以使用如sdkVersion,品牌,渠道...
 * 你可以在SamplePatchListener中解析它。
 * 然后你可以使用补丁条件!
 */
 configField("platform", "all")
 /**
 * 补丁版本通过packageConfig
 */
 configField("patchVersion", "1.0")
}
//或者您可以添加外部的配置文件,或从旧apk获取元值
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")

/**
 * 如果你不使用zipArtifact或者path,我们只是使用7za来试试
 */
sevenZip {
 /**
 * 可选,默认'7za'
 * 7zip工件路径,它将使用正确的7za与您的平台
 */
 zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
 /**
 * 可选,默认'7za'
 * 你可以自己指定7za路径,它将覆盖zipArtifact值
 */
// path = "/usr/local/bin/7za"
}
}

List<String> flavors = new ArrayList<>();
project.android.productFlavors.each {flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
 * task type, you want to bak
 */
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 {
//sample use for build all flavor for one time
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"
  }

}
 }
}
}
}

3.在清单文件中集成application和服务 ,name的application必须是.AMSKY,如果你添加不进去,或者是红色的话,请先build一下,如果你已经有了自己的application,后面我会说怎么来集成,Service中做的操作是在你加载成功热更新插件后,会提示你更新成功,并且这里做了锁屏操作就会加载热更新插件,继续往下看。


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tinker.demo.tinkerdemo">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".AMSKY"
android:theme="@style/AppTheme">

<service
 android:name=".service.SampleResultService"
 android:exported="false"/>

<activity android:name=".MainActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
</activity>
</application>

</manifest>

4.到这里就已经基本集成的差不多了,剩下的就是代码里面的集成,首先是application,这里主要说如果是自已已经存在的application的时候改怎么操作 ,这个applicaiton可以说就是自己的一个application,只不过写法,要这样去写,可以在onCreate中做自己的一些操作,只不过清单文件中,要写AMSKY


@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.tinker.demo.tinkerdemo.AMSKY",
  flags = ShareConstants.TINKER_ENABLE_ALL,
  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
private static final String TAG = "Tinker.SampleApplicationLike";

public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent,Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {

super(application,tinkerFlags,tinkerLoadVerifyFlag,applicationStartElapsedTime,applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager);

}

/**
* install multiDex before install tinker
* so we don't need to put the tinker lib classes in the main dex
*
* @param base
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//MultiDex必须在Tinker初始化之前
MultiDex.install(base);
//这里就是初始化Tinker
TinkerInstaller.install(this,new DefaultLoadReporter(getApplication()),new DefaultPatchReporter(getApplication()),
new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch());
Tinker tinker = Tinker.with(getApplication());
//这个只是一个Toast提示
Toast.makeText(
getApplication(),"没鸟用,就是Toast提示而已", Toast.LENGTH_SHORT).show();
}

@Override
public void onCreate() {
super.onCreate();
//这里可以做自己的操作
}

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}

}

5.这里就是在MainActivity中来加载热更新文件,在点击加载的时候,就直接锁屏加载(不要删除service),当然退出app,下次进来也是可以加载的吗,这里加载补丁插件的话,路径可以自己设置,我是放在根目录的debug文件夹当中的,并且我的补丁插件名字叫patch,可以自行更改。


public class MainActivity extends AppCompatActivity {

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

/**
* 加载热补丁插件
* @param v
*/
public void loadPatch(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), "/sdcard/debug/patch.apk");
}

/**
* 杀死应用加载补丁
* @param v
*/
public void killApp(View v) {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
android.os.Process.killProcess(android.os.Process.myPid());
}

@Override
protected void onResume() {
super.onResume();
Utils.setBackground(false);
}

@Override
protected void onPause() {
super.onPause();
Utils.setBackground(true);
}
}

6.Service文件


public class SampleResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.SampleResultService";

@Override
public void onPatchResult(final PatchResult result) {
if (result == null) {
 TinkerLog.e(TAG, "SampleResultService received null result!!!!");
 return;
}
TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString());

//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
 @Override
 public void run() {
 if (result.isSuccess) {
  Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
 } else {
  Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
 }
 }
});
// is success and newPatch, it is nice to delete the raw file, and restart at once
// for old patch, you can't delete the patch file
if (result.isSuccess) {
 File rawFile = new File(result.rawPatchFilePath);
 if (rawFile.exists()) {
 TinkerLog.i(TAG, "save delete raw patch file");
 SharePatchFileUtil.safeDeleteFile(rawFile);
 }
 //not like TinkerResultService, I want to restart just when I am at background!
 //if you have not install tinker this moment, you can use TinkerApplicationHelper api
 if (checkIfNeedKill(result)) {
 if (Utils.isBackground()) {
  TinkerLog.i(TAG, "it is in background, just restart process");
  restartProcess();
 } else {
  //we can wait process at background, such as onAppBackground
  //or we can restart when the screen off
  TinkerLog.i(TAG, "tinker wait screen to restart process");
  new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() {
  @Override
  public void onScreenOff() {
   restartProcess();
  }
  });
 }
 } else {
 TinkerLog.i(TAG, "I have already install the newly patch version!");
 }
}
}

/**
* you can restart your process through service or broadcast
*/
private void restartProcess() {
TinkerLog.i(TAG, "app is background now, i can kill quietly");
//you can send service or broadcast intent to restart your process
android.os.Process.killProcess(android.os.Process.myPid());
}

static class ScreenState {
interface IOnScreenOff {
 void onScreenOff();
}

ScreenState(Context context, final IOnScreenOff onScreenOffInterface) {
 IntentFilter filter = new IntentFilter();
 filter.addAction(Intent.ACTION_SCREEN_OFF);
 context.registerReceiver(new BroadcastReceiver() {

@Override
 public void onReceive(Context context, Intent in) {
  String action = in == null ? "" : in.getAction();
  TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action);
  if (Intent.ACTION_SCREEN_OFF.equals(action)) {

context.unregisterReceiver(this);

if (onScreenOffInterface != null) {
   onScreenOffInterface.onScreenOff();
  }
  }
 }
 }, filter);
}
}

}

7.Utils文件


public class Utils {

/**
* the error code define by myself
* should after {@code ShareConstants.ERROR_PATCH_INSERVICE
*/
public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL = -5;
public static final int ERROR_PATCH_ROM_SPACE  = -6;
public static final int ERROR_PATCH_MEMORY_LIMIT  = -7;
public static final int ERROR_PATCH_ALREADY_APPLY  = -8;
public static final int ERROR_PATCH_CRASH_LIMIT  = -9;
public static final int ERROR_PATCH_RETRY_COUNT_LIMIT = -10;
public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -11;

public static final String PLATFORM = "platform";

public static final int MIN_MEMORY_HEAP_SIZE = 45;

private static boolean background = false;

public static boolean isGooglePlay() {
return false;
}

public static boolean isBackground() {
return background;
}

public static void setBackground(boolean back) {
background = back;
}

public static int checkForPatchRecover(long roomSize, int maxMemory) {
if (Utils.isGooglePlay()) {
 return Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL;
}
if (maxMemory < MIN_MEMORY_HEAP_SIZE) {
 return Utils.ERROR_PATCH_MEMORY_LIMIT;
}
//or you can mention user to clean their rom space!
if (!checkRomSpaceEnough(roomSize)) {
 return Utils.ERROR_PATCH_ROM_SPACE;
}

return ShareConstants.ERROR_PATCH_OK;
}

public static boolean isXposedExists(Throwable thr) {
StackTraceElement[] stackTraces = thr.getStackTrace();
for (StackTraceElement stackTrace : stackTraces) {
 final String clazzName = stackTrace.getClassName();
 if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) {
 return true;
 }
}
return false;
}

@Deprecated
public static boolean checkRomSpaceEnough(long limitSize) {
long allSize;
long availableSize = 0;
try {
 File data = Environment.getDataDirectory();
 StatFs sf = new StatFs(data.getPath());
 availableSize = (long) sf.getAvailableBlocks() * (long) sf.getBlockSize();
 allSize = (long) sf.getBlockCount() * (long) sf.getBlockSize();
} catch (Exception e) {
 allSize = 0;
}

if (allSize != 0 && availableSize > limitSize) {
 return true;
}
return false;
}

public static String getExceptionCauseString(final Throwable ex) {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final PrintStream ps = new PrintStream(bos);

try {
 // print directly
 Throwable t = ex;
 while (t.getCause() != null) {
 t = t.getCause();
 }
 t.printStackTrace(ps);
 return toVisualString(bos.toString());
} finally {
 try {
 bos.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
}
}

private static String toVisualString(String src) {
boolean cutFlg = false;

if (null == src) {
 return null;
}

char[] chr = src.toCharArray();
if (null == chr) {
 return null;
}

int i = 0;
for (; i < chr.length; i++) {
 if (chr[i] > 127) {
 chr[i] = 0;
 cutFlg = true;
 break;
 }
}

if (cutFlg) {
 return new String(chr, 0, i);
} else {
 return src;
}
}
}

到这里就已经集成完毕,下面来说下使用的方法

这是有bug的版本,我们测试就使用assembleDebug来测试 ,注意没点击assembleDebug之前,build文件夹里面是没有bakApk文件夹的

Android微信Tinker热更新详细使用Android微信Tinker热更新详细使用

2.点击assembleDebug之后会出现bakApk这个文件夹,里面就有apk文件,如果失败,记得clean一下,然后build一下

Android微信Tinker热更新详细使用

3.接下来在build文件夹里面,更改ext中的属性,将bakApk中生成的apk文件和R文件复制到ext这里,如果你打的release包有mapping的话同样复制到这里,我们这里是debug测试,所以没有mapping文件

Android微信Tinker热更新详细使用

4.下面就修改我们需要更新,或者更改的bug,我这里是添加一张图片,并且更改标题显示

这是有bug的版本,我还没添加图片,更改标题

Android微信Tinker热更新详细使用

这里我添加了一张aa的图片,并且更改了标题

Android微信Tinker热更新详细使用

5.接下来我们运行tinker下面的tinkerPatchDebug,来生成补丁包,这个补丁包在outputs下面

Android微信Tinker热更新详细使用

点击完成后,就会生成tinkerPatch文件夹

Android微信Tinker热更新详细使用

将tinkerPatch文件夹下面的patch_signed_7zip.apk文件,粘贴出来,改成你的MainActivity中加载的文件名字,我这里叫patch,然后点击加载没加载之前

Android微信Tinker热更新详细使用

加载之后,锁频,解锁 ,补丁已经加载出来了,并且文件夹中的补丁已经不在了,因为它和老apk合并了

Android微信Tinker热更新详细使用

注意

签名文件的话 在build的signingConfigs中设置,以及左侧的kestore文件夹中设置 ,如下图
Android微信Tinker热更新详细使用
Android微信Tinker热更新详细使用

项目github地址:TinkerDemo

Tinker原项目地址https://github.com/Tencent/tinker
Tinker使用指南:https://github.com/Tencent/tinker/wiki
Tinker一键集成(这个简单,但是不能从自己服务器上下载补丁,不需配置Tinker自己的后台,有部分局限性,自行选择):https://github.com/TinkerPatch/tinkerpatch-sdk/blob/master/docs/tinkerpatch-android-sdk.md
Tinker一键集成后台http://www.tinkerpatch.com/

更多精彩内容请点击《Android微信开发教程汇总》,《java微信开发教程汇总》欢迎大家学习阅读。

标签:微信,Tinker,热更新
0
投稿

猜你喜欢

  • SpringBoot利用注解来实现Redis分布式锁

    2022-02-13 16:57:36
  • Spring定时任务使用及如何使用邮件监控服务器

    2023-01-12 16:38:58
  • Android webview旋转屏幕导致页面重新加载问题解决办法

    2021-10-18 17:07:35
  • android教程之把自己的应用加入到系统分享中

    2022-05-13 11:38:41
  • 详解java中产生死锁的原因及如何避免

    2022-04-22 00:36:14
  • Android Button按钮的四种点击事件

    2021-12-16 04:02:41
  • JavaWeb中struts2实现文件上传下载功能实例解析

    2021-11-23 22:53:15
  • Java动态代理静态代理实例分析

    2023-11-14 18:06:07
  • Java使用Preference类保存上一次记录的方法

    2023-05-08 01:49:14
  • C#同步和异步调用方法实例

    2022-09-11 21:20:50
  • springboot 同时启用http/https的配置方法

    2023-06-22 12:04:12
  • 仅用5分钟极速入门Dubbo使用教程

    2022-08-08 12:08:55
  • Android开发中自定义 editText下划线

    2023-03-30 13:40:35
  • Java String对象使用方法详解

    2023-12-14 14:43:52
  • 基于WPF实现简单放大镜效果

    2022-02-15 23:19:12
  • 基于SPRINGBOOT配置文件占位符过程解析

    2021-06-27 04:25:12
  • MyBatis3用log4j在控制台输出SQL的方法示例

    2023-07-01 07:32:36
  • springboot中的springSession的存储和获取实现

    2023-11-04 10:23:14
  • SpringBoot的@Value注解如何设置默认值

    2023-09-03 14:32:05
  • C#8.0中的模式匹配

    2023-07-19 13:27:39
  • asp之家 软件编程 m.aspxhome.com