我用四天时间改了一行代码【永利澳门游戏网站】,热修复框架Tinker最完整讲解

近年来光景分来了多少个bug,该bug很诡吊,一行代码的退换,花了八日时间,因为该Bug会block出货,项目首席试行官每日催问进程,时期真是以为一切人都倒霉了。本篇博文记录化解该难点的不利进程,因为代码保密条件,本事细节不便详细给出,入眼记录消除难题的经过。

转发请注解出处:

德州仪器平台双摄拍完的景深图片在Gallery内,能张开refocus操作,Camera最近新扩张拍录4:3百分比的图纸,refocus时该种比例的图形显示会拉伸,为了化解该难点,小编对refocus后的图样张开了三个resize操作。之后就报出对该种图片进行refocus操作,相当高的概率出现点击四次技术科学更换图片核心的bug。16:9的图形则从未该Bug。现象如图-1.

2017/09/22
更新:为了同步Tinker官方,故将github上的德姆o中的Tinker版本晋级到了1.8.1(不要使用1.8.0的版本,该版本不扶助加固)。

永利澳门游戏网站 1图-1

近日大家的产品又革新了二个本子,当二个测量试验把产品上传了贰13个应用市镇的时候,其他三个测验顿然测出一个根本bug。项目组长就说把一切沟渠上传的先退回来,重新改好bug再揭橥。那时候大家就苦逼了,又要走二次具名打包->加固->签名->上传应用市场的流程。要通晓大家的水渠有附近19个,整个流程又要花上2,3小时。只好全部加班加点了。。。

而正规的情景应该点击哪个地方,哪儿就一清二楚。如图-2.

为了化解有三个bug又要双重发表版本的难题,项目经理就叫本人赶忙把热修复集成到项目中去。一开端认为3天就可以消除,就跟项目老董要了4天时间。结果是全数搞了5天!集成tinker
德姆o确实一天能够化解,可是还要自定义Application类(我们项目标Application还比较复杂),集成美团Walle多门路打包(大家从前多路子打包是选用productFlavors实现的),与后台沟通接口的布署,svn的分支管理,测验整个流程等等就花了非常多光阴了。这之中境遇了比比较多坑和难题。这么些体系小说都会相继解说,菜鸟看了迟早会少走比非常多弯路。

永利澳门游戏网站 2图-2

Tinker是微信官方的Android热补丁技术方案,它帮忙动态下发代码、So库以及能源,让使用能够在无需重新安装的景况下促成立异。当然,你也可以动用Tinker来更新您的插件。

标题是在自家的更动未来才掀起的,由此那个漏洞还得自身来补,回落掉resize的改造,该难点确实没有了,但图片会被拉伸的标题理当如此又会出去,看来作者得找其余措施考订图片拉伸的主题素材,此时项目首席营业官发出邮件,强调该难题会block产品出货,让尽快管理。顶着压力最早撸二次代码,梳理了下Refocus的差不多进度:

当前商场的热补丁方案有众多,在那之中相比较出名的有Ali的AndFix、美团的Robust以及QZone的最好补丁方案。但它们都设有无法缓慢解决的主题材料,那也是幸而大家生产Tinker的源委。

永利澳门游戏网站 3

永利澳门游戏网站 4

因为难点只现出在4:3比例的图片上,一最早就以为难点出在A进度中,预计4:3百分比的图形接收的touch坐标与图片上的坐标点未有科学对应。排查代码,开掘底层算法逻辑是按16:9的图片比例管理景深数据,到此以为找到了root
cause,连同图片会被拉伸的标题一并抛给平台,让她们去解。因为是平台提供的算法难题,而算法主题逻辑平台是以库文件release出来,大家无可奈何修改。难题也抛给了平台,笔者那边未有压力,只用等平台提供修复patch过来合并就可。但是等待一段时间后,平台给了反映,小编事先为化解图片拉伸引进的resize
patch修改方案是卓有成效的,他们查对拉伸难题的改法与本身的同样,重视是丰裕resize的patch该难点在平台的Gallery上无助复现,那的确将标题抛回给了自己。压力指数应声暴增十多个点,纵然平台没不通常,这就只或然是自家在porting平台gallery的时候出错了。这里表明下项目采用的gallery并非依据平台Gallery开采的,只是将平台gallery上的refocus相关代码porting进来。就算porting过来的代码爆发bug。就有三种恐怕:

如上所述:1、AndFix作为native应用方案,首先面临的是平静与包容性难点,更要紧的是它不可能落实类替换,它是亟需大批量附加的开垦开支的;2、罗布ust宽容性与成功率较高,不过它与AndFix同样,不可能新扩展变量与类只可以用做的bugFix方案;3、Qzone方案能够变成公布产品功用,不过它至关心重视要难题是插桩带来Dalvik的天性难题,以及为了缓和Art下内部存款和储蓄器地址难题而招致补丁包飞快增大的。

  • 平台gallery问题
  • porting进程中引进的难题

非常是在Android
N之后,由于混合编译的inline计策修改,对于市情上的各样方案都不太轻松消除。而Tinker热补丁方案不止补助类、So以及财富的更迭,它照旧2.X-7.X的全平台帮衬。利用Tinker大家不光能够用做bugfix,甚至足以代表成效的发表。Tinker已运维在微信的数亿Android设备上,那么为啥您不接纳Tinker呢?

真出标题了,当然希望是首先类主题素材,那样最最少还能够求助平台,要是是porting不当引发的标题,平台顶多帮衬深入分析下,还得本身去一丢丢嚼回头草,今后假若平台未能复现难点,那么一切压力就又跑到自家那边来了。于是赶紧更新平台的Gallery代码,安装后测量试验,作者擦,明显能复现啊。那是怎么回事呢?又再一次拿平台的gallery复测了四回,确实平台gallery也是有该难题。于是下班前再次把场景发邮件给平台,那时距离项目经理发出邮件已经过去了一天。

由于原理与系统限制,Tinker有以下已知难题:1、Tinker不扶助修改AndroidManifest.xml,Tinker不帮衬新添四大组件;2、由于GooglePlay的开荒者条约范围,不提出在GP门路动态更新代码;3、在Android
N上,补丁对使用运转时间有轻微的熏陶;4、不帮忙部分Samsungandroid-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall
failed”;5、对于财富替换,不援救修改remoteView。例如transition动画,notification
icon以及桌面图标。

Day-1后记非常不足自信,浪费了许多日子复测平台gallery的情形,由于平台反映他们走入resize
patch都未能复现难题,因而就打结自个儿向平台走入resize的patch不正确,检查了少数遍代码,在丰硕编写翻译安装测验,浪费了光阴。过度的明细,浪费时间,拖慢了进程。

以上有关Tinker的牵线来自Tinker官方Wiki

发送给平台的邮件获得上升,平台照旧不可能复现难题,难点陷入罗生门。笔者用平台的Gallery能复现问题,但平台却又力不从心复现。于是又梳理了多少个恐怕形成结果分裂的疑心点。

一、配置gradle

#Tinker版本号TINKER_VERSION=1.7.11

buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.2.2' //tinker classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}" }}

dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.1.1' // 多dex 打包的类库 compile 'com.android.support:multidex:1.0.1' //tinker的核心库 compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true } //用于生成application类 provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }}

上面就把方方面面app/build.gradle配置贴出来,在那之中标有Tinker相关配置即为tinker的相关布置,当中相比较首要的习性都标有普通话注释,其他属性解释能够活动参照他事他说加以考察Tinker 接入指南

apply plugin: 'com.android.application'android { compileSdkVersion 23 buildToolsVersion "23.0.2" //recommend Tinker相关配置 dexOptions { jumboMode = true } //签名信息配置 signingConfigs { release { storeFile file("./keystore/wildmatinker.jks") keyAlias "wildmatinker" storePassword "123456" keyPassword "123456" } debug { storeFile file("./keystore/debug.keystore") } } defaultConfig { applicationId "com.wildma.wildmatinker" minSdkVersion 14 targetSdkVersion 22 versionCode 1 versionName "1.0.0" //Tinker相关配置start====================================== testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" /** * you can use multiDex and install it in your ApplicationLifeCycle implement */ multiDexEnabled true /** * buildConfig can change during patch! * we can use the newly value when patch */ buildConfigField "String", "MESSAGE", ""I am the base apk""// buildConfigField "String", "MESSAGE", ""I am the patch apk"" /** * client version would update with patch * so we can get the newly git version easily! */ buildConfigField "String", "TINKER_ID", ""${getTinkerIdValue()}"" buildConfigField "String", "PLATFORM", ""all"" //Tinker相关配置end====================================== } 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(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.1.1' // 多dex 打包的类库 compile 'com.android.support:multidex:1.0.1' //tinker的核心库 compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true } //用于生成application类 provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }}//Tinker相关配置start======================================def gitSha() { try {// String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim() String gitRev = '1.0.0' //tinkerID,每次发布新版本都需要修改! 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 javaVersion = JavaVersion.VERSION_1_7def 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 { //是否打开tinker的功能 tinkerEnabled = true //old apk地址 tinkerOldApkPath = "${bakPath}/app-release-0708-21-59-49.apk" //old apk 混淆文件地址 tinkerApplyMappingPath = "${bakPath}/app-release-0708-21-59-49-mapping.txt" //old apk R 文件地址 tinkerApplyResourcePath = "${bakPath}/app-release-0708-21-59-49-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 { /** * 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 //是否开启加固 isProtectedApp = 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 //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.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.toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length 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 dependsOn tinkerTask def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest") preAssembleTask.doFirst { String flavorName = preAssembleTask.name.substring.toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length 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" } } } } }}//Tinker相关配置end======================================
  • 复现难点的图纸。因为refocus接纳的图形注重Camera拍片,会不会平台复现难题时所用的图样和我们分歧导致的吧。邮件将小编复现用的图纸发送给平台方,得知依旧力不能支复现。
  • 自己投入的resize
    patch与平台分化。邮件将本身的改动发送给平台方,经确认修改地方一样。
  • porting时出错。邮件将porting过来的refocus相关文件发送给平台方,扶助确认porting是还是不是有标题,经确认porting准确。
  • 邮件发送log给平台,请其尝试从log排查是还是不是有标题。自身和平台都未看见难点。

二、自定义Application类

程序运转时会加载暗许的Application类,那形成补丁包无法对它做修改。所以Tinker官方说不提议协调去贯彻Application,而是由Tinker自动生成。即须求创造多个SampleApplication类,承继DefaultApplicationLike,然后将我们温馨的MyApplication中负有逻辑放在萨姆pleApplication中的onCreate中。最终索要将大家项目中此前的MyApplication类删除。如下:

package com.wildma.wildmatinker;import android.annotation.TargetApi;import android.app.Application;import android.content.Context;import android.content.Intent;import android.os.Build;import android.support.multidex.MultiDex;import com.tencent.tinker.anno.DefaultLifeCycle;import com.tencent.tinker.lib.tinker.Tinker;import com.tencent.tinker.lib.tinker.TinkerInstaller;import com.tencent.tinker.loader.app.DefaultApplicationLike;import com.tencent.tinker.loader.shareutil.ShareConstants;import com.wildma.wildmatinker.tinker.MyLogImp;import com.wildma.wildmatinker.tinker.TinkerManager;@SuppressWarnings@DefaultLifeCycle( application = "com.wildma.wildmatinker.MyApplication",// 自定义生成 flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)public class SampleApplication extends DefaultApplicationLike { public static SampleApplication sampleApplication; public SampleApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) { super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent); } @Override public void onCreate() { super.onCreate(); sampleApplication = this; //将我们自己的MyApplication中的所有逻辑放在这里,例如初始化一些第三方 } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached; // 其原理是分包架构,所以在加载初要加载其余的分包 MultiDex.install; // Tinker管理类,保存当前对象 TinkerManager.setTinkerApplicationLike; // 崩溃保护 TinkerManager.initFastCrashProtect(); // 是否重试 TinkerManager.setUpgradeRetryEnable; //Log 实现,打印加载补丁的信息 TinkerInstaller.setLogIml(new MyLogImp; // 运行Tinker ,通过Tinker添加一些基本配置 TinkerManager.installTinker; Tinker tinker = Tinker.with(getApplication; } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { // 生命周期,默认配置 getApplication().registerActivityLifecycleCallbacks; } /** * 获取SampleApplication实例 * @return */ public static SampleApplication getSampleApplication(){ return sampleApplication; }}

内部DefaultLifeCycle中的MyApplication为大家实在的Application,清单文件中的Application的name改为MyApplication的全路线。如下:

<application android:name="com.wildma.wildmatinker.MyApplication" 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>

标题现今认为不可能展开下去了,平台不能够复现难点,也就无可奈何交付技术方案。自身转向去化解任何bug。该难题高居用不了结的办法去了结状态。那时距离项目老董发出邮件过去了两日。

三、添加Tinker相关类

这一个类主假使加载补丁包进程的回调,打字与印刷log,崩溃珍视等。具体见 Tinker
自定义扩大

永利澳门游戏网站 5

Day-2后记1.沟通不畅,当本身那边复测后场景与平台方不等同,应该即刻联系,这点在Day-3中经过项目CEO的递进才加紧了牵连步伐。2.并未有从正面解决难题。一向围绕难题广泛在排查,本来梦想那样能便捷定位难题,但未能第有时间再度详细梳理代码逻辑。3.惰性发生。高烧的bug未有一些端倪后,发生懈怠思想。搁置的难点长久是难点,出现了要尊重消除。

四、在清单文件增添读写sd卡的权杖

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注