Earlier we introduced Sophix Solution, an online hot update solution from Ali. But the need is always motivating us to move forward, and here is a scenario. The boss said: “Miao Sen, give us the whole APP hot update.” I said, “Boss, let’s get Sophix.” The boss says, “What? Ali’s? No more fees.” Me: “…” Boss: “and we are the application of Intranet special network, connect less than outside network.” Me: “…” So motivated by demand, I’m here to introduce You to Tinker’s fool-follow access solution.
1. Modify the version
Our Tinker doesn’t support Gradle higher versions very well, so let’s change Gradle version here
The Gradle version I have here is referenced below
The classpath "com. Android. Tools. Build: gradle: 3.4.2." " distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zipCopy the code
2. Build interfaces
Here is not clear, mainly for testing, but also according to the actual situation of their own projects. Here, I mainly have a button and a TextView. The button is to join the action of loading Patch later, and the TextView is to simulate the content modification.
3. Change the packaging mode
Add or modify the following code in your app’s build.gradle
buildTypes { release { minifyEnabled false signingConfig signingConfigs.debug proguardFiles 'proguard-rules.pro' } debug { minifyEnabled false signingConfig signingConfigs.debug proguardFiles 'proguard-rules.pro' } } signingConfigs { debug {storeFile file(' your signature path ') storePassword 'signed storePassword' keyAlias = 'signed keyAlias' keyPassword' signed keyPassword'}} compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }Copy the code
4. Add a platform
Add the following code under the BuildScript -> Dependencies node of your project in build.gradle
Classpath (' com. Tencent. Tinker: tinker - patch - gradle - plugin: 1.9.1 ')Copy the code
5. Add Tinker dependencies
/ / dex configuration the compile "com. Android. Support: multidex:" 1.0.1 / / Tinker Implementation (" com. Tencent. Tinker: tinker - android - lib: 1.9.14.5 ") {changing = true} AnnotationProcessor (" com. Tencent. Tinker: tinker - android - anno: 1.9.14.5 ") {changing = true} CompileOnly (" com. Tencent. Tinker: tinker - android - anno: 1.9.14.5 ") {changing = true} / / implementation 'com. Permissionx. Guolindev: permission - support: 1.4.0'Copy the code
In fact, in addition to the dependence of Tinker, we rely on dex subcontracting and a framework of permission application written by Guo Lin. We can make corresponding choices according to the actual situation of our own projects.
6. Let’s compile the project and import corresponding packages
7. Next we import Tinker Gradle scripts
def gitSha() {
return "app-1.0.0";
}
def bakPath = file("${buildDir}/bakApk/")
/**
* you can use assembleRelease to build you base apk
* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
* add apk from the build/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-0126-12-51-41.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-0126-12-51-41-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0126-12-51-41-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") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/
oldApk = getOldApkPath()
/**
* optional,default 'false'
* there are some cases we may get some warnings
* if ignoreWarning is true, we would just assert the patch process
* case 1: minSdkVersion is below 14, but you are using dexMode with raw.
* it must be crash when load.
* case 2: newly added Android Component in AndroidManifest.xml,
* it must be crash when load.
* case 3: loader classes in dex.loader{} are not keep in the main dex,
* it must be let tinker not work.
* case 4: loader classes in dex.loader{} changes,
* loader classes is ues to load patch dex. it is useless to change them.
* it won't crash, but these changes can't effect. you may ignore it
* case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
*/
ignoreWarning = false
/**
* optional,default 'true'
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/
useSign = true
/**
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = buildWithTinker()
/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* optional,default 'null'
* if we use tinkerPatch to build the patch apk, you'd better to apply the old
* apk mapping file if minifyEnabled is enable!
* Warning:
* you must be careful that it will affect the normal assemble build!
*/
applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* necessary,default 'null'
* because we don't want to check the base apk with md5 in the runtime(it is slow)
* tinkerId is use to identify the unique base apk when the patch is tried to apply.
* we can use git rev, svn rev or simply versionCode.
* we will gen the tinkerId in your manifest automatic
*/
tinkerId = getTinkerIdValue()
/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
*/
keepDexApply = false
/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
* 如果是加固的场景这里设置为true
*/
isProtectedApp = false
/**
* optional, default 'false'
* Whether tinker should support component hotplug (add new component dynamically).
* If this attribute is true, the component added in new apk will be available after
* patch is successfully loaded. Otherwise an error would be announced when generating patch
* on compile-time.
*
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/
supportHotplugComponent = false
}
dex {
/**
* optional,default 'jar'
* only can be 'raw' or 'jar'. for raw, we would keep its original format
* for jar, we would repack dexes with zip format.
* if you want to support below 14, you must use jar
* or you want to save rom or check quicker, you can use raw mode also
*/
dexMode = "jar"
/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
/**
* optional,default '[]'
* what library in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* for library in assets, we would just recover them in the patch directory
* you can get them in TinkerLoadResult with Tinker
*/
pattern = ["lib/*/*.so"]
}
res {
/**
* optional,default '[]'
* what resource in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* you must include all your resources in apk here,
* otherwise, they won't repack in the new apk resources.
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* the resource file exclude patterns, ignore add, delete or modify resource change
* it support * or ? pattern.
* Warning, we can only use for files no relative with resources.arsc
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* for modify resource, if it is larger than 'largeModSize'
* we would like to use bsdiff algorithm to reduce patch file size
*/
largeModSize = 100
}
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* if you don't use zipArtifact or path, we just use 7za to try
*/
sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
def date = new Date().format("MMdd-HH-mm-ss")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
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.first().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"
}
}
}
}
}
}
task sortPublicTxt() {
doLast {
File originalFile = project.file("public.txt")
File sortedFile = project.file("public_sort.txt")
List<String> sortedLines = new ArrayList<>()
originalFile.eachLine {
sortedLines.add(it)
}
Collections.sort(sortedLines)
sortedFile.delete()
sortedLines.each {
sortedFile.append("${it}\n")
}
}
}
Copy the code
8. Add permissions to the manifest file
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Copy the code
9. Create the Application class as follows
@DefaultLifeCycle(
application = "com.hao.csdn.MyApplication", //application name to generate
flags = ShareConstants.TINKER_ENABLE_ALL)
public class MyApplicationLike extends DefaultApplicationLike {
public MyApplicationLike(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);
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(this
,new DefaultLoadReporter(getApplication())
,new DefaultPatchReporter(getApplication())
,new DefaultPatchListener(getApplication())
,SampleResultService.class
,upgradePatchProcessor);
Tinker tinker = Tinker.with(getApplication());
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}
Copy the code
Application = “com. Hao. CSDN. MyApplication” this application it is not our real file, as long as we specify a path can we package name. Just replace com.hao. CSDN with your package name.
SampleResultService = SampleResultService = SampleResultService = SampleResultService
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) {// Succeeded in doing something Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show(); } else {// fail to do something toast.maketext (getApplicationContext(), "patch fail, please check reason", toast.length_long).show(); }}}); } /** * 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()); }}Copy the code
As you can see in the code, this contains what we’re rewriting hot update success and failure to do. We can customize according to the actual needs of the business.
11. In the listing file application node configuration we generate com. Hao. CSDN. MyApplication
android:name=".MyApplication"
Copy the code
12. Compile once, here mainly to build just the specified com. Hao. CSDN. MyApplication
13. Add the Service you just wrote to the manifest file
<service android:name=".SampleResultService"/>
Copy the code
14. Add the method of loading Patch
We add the following method where patch needs to be loaded
/** * Loading patch */ private void loadPatch(){File patchFile = new File(Environment.getExternalStorageDirectory()+"/longway/","patch_signed_7zip.apk"); if(! patchFile.exists()){ Toast.makeText(getApplicationContext(),"patchFile not exist!" , Toast.LENGTH_SHORT).show(); return; }else{toast.maketext (this," have ", toast.length_short).show(); } TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),patchFile.getAbsolutePath()); }Copy the code
We then call loadPatch() when the button is clicked, or if our own application requires it.
This method defines a path in the Environment. The external.getexternalstoragedirectory () + “/ longway/”,” patch_signed_7zip. Apk “can be defined according to their own situation.
15. Add the following code to gradle.properties
org.gradle.jvmargs = -Dfile.encoding=UTF-8
Copy the code
16. Run the project
17. Set the base package
At this point in our app -> Build -> bakApk we will have our own related packaging file. We just found it in build.gradle of our app
Let’s change the red box to the content we just generated by packaging in the folder
18. Compile the project
19. Create a patch package
In the diagram, we find tinkerPatchDebug, double-click tinkerPatchDebug if our base package is Debug, double-click tinkerPatchRelease if it is Release. Of course, before you do that, you have to change some code, so I changed the contents of the TextView.
20. Find the patch package
At this point, after the system packaging process, we will see the message “BUILD SUCCESSFUL in 12s” on the console, indicating that we have completed the packaging. We found patch_signed_7zp.apk in app -> build -> outputs -> APK -> tinkerPatch -> debug
And place the files in the folder we defined in step 14. We can optimize this step by downloading it, but we won’t show you how to download it here.
21. At this point, our work is done. After executing the loadPatch method in step 14, we can load our patch. The patch will take effect when the user restarts the app. One thing I would note here is that we need to apply for dynamic permissions before loading the patch. Otherwise the app will report an error with code:-2.
Ok, this is the end of Tinker access, if you have any questions and comments can be discussed with me in the comments section.