Android 위 챗 Tinker 핫 업데이트 상세 사용
질문
원리 와 시스템 제한 으로 인해 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:공식 적 인 집성 은 매우 번 거 로 워 서 하루 종일 정 해 지지 않 을 수도 있 습 니 다.자신의 수요 와 상황 에 따라 추가 합 니 다.마지막 에 데모 가 올 라 갑 니 다.
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.목록 파일 에 애플 리 케 이 션 과 서 비 스 를 통합 합 니 다.name 의 애플 리 케 이 션 은 반드시.AMSKY 여야 합 니 다.만약 에 추가 할 수 없 거나 빨간색 이 라면 먼저 build 하 십시오.만약 에 자신의 애플 리 케 이 션 이 있다 면 그 다음 에 제 가 어떻게 통합 하 는 지 말씀 드 리 겠 습 니 다.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 일 때 어떻게 조작 하 는 지 를 말 합 니 다.이 application 은 자신의 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.서비스 파일
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 가 있 는 버 전 입 니 다.저 희 는 assemble Debug 를 사용 하여 테스트 합 니 다.assemble Debug 를 누 르 기 전에 build 폴 더 에 bakApk 폴 더 가 없습니다.
2.assemble Debug 를 누 르 면 bakApk 라 는 폴 더 가 나타 납 니 다.안에 apk 파일 이 있 습 니 다.실패 하면 clean 을 기억 하고 build 를 하 십시오.
3.다음 build 폴 더 에서 ext 의 속성 을 변경 합 니 다.bakApk 에서 생 성 된 apk 파일 과 R 파일 을 ext 에 복사 합 니 다.만약 에 release 패키지 에 mapping 이 있다 면 여기 로 복사 합 니 다.debug 테스트 이기 때문에 mapping 파일 이 없습니다.
4.업데이트 하거나 변경 해 야 할 bug 를 수정 합 니 다.그림 을 추가 하고 제목 표 시 를 변경 합 니 다.
이것 은 bug 가 있 는 버 전 입 니 다.저 는 아직 그림 을 추가 하지 않 았 습 니 다.제목 을 바 꾸 었 습 니 다.
여기에 aa 그림 을 추가 하고 제목 을 바 꿨 습 니 다.
5.다음 에 tinker 아래 에 있 는 tinkerPatchDebug 를 실행 하여 패 치 패 치 를 만 듭 니 다.이 패 치 는 outputs 아래 에 있 습 니 다.
클릭 이 완료 되면 tinkerPatch 폴 더 가 생 성 됩 니 다.
tinkerPatch 폴 더 아래 patchsigned_7zip.apk 파일,붙 여 넣 기,MainActivity 에서 불 러 온 파일 이름 으로 바 꿉 니 다.저 는 patch 라 고 합 니 다.그리고 불 러 오기 전에 클릭 하 십시오.
불 러 온 후 잠 금 주파수,잠 금 해제,패 치 를 불 러 왔 습 니 다.폴 더 에 있 는 패 치가 없습니다.오래된 apk 와 합 쳐 졌 기 때 문 입 니 다.
주의 하 다.
파일 에 서명 하면 build 의 signingConfigs 에 설정 하고 왼쪽 kestore 폴 더 에 설정 합 니 다.다음 그림 입 니 다.
프로젝트 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 위 챗 개발 튜 토리 얼 집계','자바 위 챗 개발 튜 토리 얼 집계'을 클릭 하여 읽 기 를 환영 합 니 다.
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
콩짜개 검색 - 위 챗 공공 플랫폼 접속 (wechatpy)위의 글 은 위 챗 공공 플랫폼 을 어떻게 연결 하 는 지 소 개 했 지만 그 안의 검증 코드 는 우리 가 스스로 실현 한 것 이다.그러나 지금 우 리 는 더 좋 은 선택 이 생 겼 다.위 챗 (WeChat) 퍼 블...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.