首先在Module:app/build.gredle下添加依赖集成SDK
如果只想集成单独的异常上报可以将依赖改成这样compile 'com.tencent.bugly:crashreport:latest.release'
添加依赖.png
然后在Module:app/build.gredle下设置支持的SO库架构集成NDK,同时配置打开多dex
设置SO库.png
然后仍然在在Module:app/build.gredle下设置配置签名(当然测试时可以不配置),同时也在打开了混淆配置以及优化(当然也可以不需要配置混淆,此处配置混淆后面会提到上传mapping快速混淆配置请参考我以前的文章)
签名并且混淆.png
然后仍然在在Module:app/build.gredle下设置配置lint检测编译器和运行时错误(当然此处也可以不配置),在这里我配置了多渠道打包,不配置也没关系,建议配置不配置都试一下,看看两个打补丁时会有什么不同
linty以及多渠道.png
好了,下来这一点需要注意,刚开始没添加这个东西,打好补丁后一直报错误,提示补丁版本不对如图:
tinker.png
后面添加了这句打补丁后就可以了
gradle.project.png
好了,到这里啦异常统计肯定没问题了,看看初始化
/** * 单个异常上传统计时的初始化方法 *@appContext上下文 *@crashReportAppId注册时申请的APPID *@isDebug自定义日志将会在Logcat中输出。 */CrashReport.initCrashReport(getApplicationContext(), Commont.APP_ID,false);/** * 统一初始化Bugly产品,包含Beta *@context上下文 *@appId注册时申请的APPID *@isDebug自定义日志将会在Logcat中输出。 * * 提示:已经接入Bugly用户改用上面的初始化方法,不影响原有的crash上报功能; init方法会自动检测更新,不需要再手动调用Beta.checkUpgrade(), 如需增加自动检查时机可以使用Beta.checkUpgrade(false,false); * 参数1:isManual 用户手动点击检查,非用户点击操作请传false * 参数2:isSilence 是否显示弹窗等交互,[true:没有弹窗和toast] [false:有弹窗或toast] */Bugly.init(this, Commont.APP_ID,true);/** * 统一初始化Bugly产品,包含Beta *@context上下文 *@appId注册时申请的APPID *@isDebug自定义日志将会在Logcat中输出。 *@strategyBugly高级设置 */Bugly.init(this, Commont.APP_ID,true, strategy);
上面有三种初始化的方法:
① 第一种为当你只需要只是仅仅要使用该产品的异常上传是依赖添加为上面说过的compile 'com.tencent.bugly:crashreport:latest.release',初始化时就使用第一种方法
②第二种为设置已经用到了统一的初始化方法,版本也会自动检查更新,异常也会上报
③拥有了第二种方法的能力并且还有其他能力,如果现在想添加一个App的打包渠道怎么搞,就用到了最后一个参数strategy作为Bugly高级设置。。。
到这里了,可以说应用升级也基本已经完成了,我们在到Manifest里面为升级补全配置
在res文件下创建新文件xml并创建文件provider_paths.xml
ok,升级也完成了, 看看最后打补丁-----热修复
我这里用到了第三方的一个插件,方便简单一点,我们首先去下载一个插件Bugly
AbdroidStudio插件下载.png
完了之后就再根目录下创建一个tinker-support.gradle,然后再配置里面的东西
tinker-support.gradle.png
详细配置:
apply plugin:'com.tencent.bugly.tinker-support'def bakPath = file("${buildDir}/bakApk/")/** * 此处填写每次构建生成的基准包目录 */def baseApkDir ="app-0119-10-02-31"/** * 对于插件各参数的详细解析请参考 */tinkerSupport { // 开启tinker-support插件,默认值trueenable=true// tinkerEnable功能开关 tinkerEnable =true// 是否编译完成后,归档apk到指定目录,默认值false// 指定归档目录,默认值当前module的子目录tinker autoBackupApkDir ="${bakPath}"// 是否启用覆盖tinkerPatch配置功能,默认值false// 开启后tinkerPatch配置不生效,即无需添加tinkerPatch overrideTinkerPatchConfiguration =true// 编译补丁包时,必需指定基线版本的apk,默认值为空 // 如果为空,则表示不是进行补丁包的编译 // @{link tinkerPatch.oldApk } baseApk ="${bakPath}/${baseApkDir}/app-release.apk"// 对应tinker插件applyMapping baseApkProguardMapping ="${bakPath}/${baseApkDir}/app-release-mapping.txt"// 对应tinker插件applyResourceMapping baseApkResourceMapping ="${bakPath}/${baseApkDir}/app-release-R.txt"// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性// tinkerId ="1.0.0.4-base"//生成对于的基包版本 只有生成基准包的时候使用 tinkerId ="1.0.0.4-patch"//对于生成的补丁包版本 只有生成补丁包的时候使用 // 构建多渠道补丁时使用 buildAllFlavorsDir ="${bakPath}/${baseApkDir}"// 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持) // isProtectedApp =true// 是否开启反射Application模式 enableProxyApplication =false// 是否支持新增非export的Activity(注意:设置为true才能修改AndroidManifest文件) supportHotplugComponent =true}/** * 一般来说,我们无需对下面的参数做任何的修改 * 对于各参数的详细介绍请参考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */tinkerPatch { tinkerEnable =true//oldApk ="${bakPath}/${appName}/app-release.apk"ignoreWarning =false//对应tinker插件userSign, 在运行过程中, // 我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。 useSign =truedex { dexMode ="jar"pattern = ["classes*.dex"] loader = [] } lib { pattern = ["lib/*/*.so"] } res { pattern = ["res/*","r/*","assets/*","resources.arsc","AndroidManifest.xml"] ignoreChange = [] largeModSize = 100 } // 用于生成补丁包中的'package_meta.txt'文件 packageConfig {// configField("patchMessage","tinker is sample to use")// configField("platform","all")// configField("patchVersion","1.0.5") } sevenZip { zipArtifact ="com.tencent.mm:SevenZip:1.1.10"// path ="/usr/local/bin/7za"} buildConfig { keepDexApply =false//tinkerId ="1.0.1-base"//applyMapping ="${bakPath}/${appName}/app-release-mapping.txt"// 可选,设置mapping文件,建议保持旧apk的proguard混淆方式 //applyResourceMapping ="${bakPath}/${appName}/app-release-R.txt"// 可选,设置R.txt文件,通过旧apk文件保持ResId的分配 }}
在上述代码中tinkerSupport{}里面有这样一句,很清楚,如果你的项目没有用到多渠道打包,那么你可以将这一句注释掉,不用管它
构建渠道补丁.png
还有这样一句代码:
构建渠道补丁.png
这种情况下分为两种,enableProxyApplication默认为false
当enableProxyApplication为false时
① 新建SampleApplicationLike继承于DefaultApplicationLike,实现构造方法,在将Application中所有要执行的操作复制到这个类中
② 新建SampleApplication集成于TinkerApplication,构造这样写
publicSampleApplication(){//记住此处com.suchengkeji.android.bugly.applications.SampleApplicationLike改为你自己新建的路径包名super(ShareConstants.TINKER_ENABLE_ALL,"com.suchengkeji.android.bugly.applications.SampleApplicationLike","com.tencent.tinker.loader.TinkerLoader",false); }@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)@OverridepublicvoidonBaseContextAttached(Context base){super.onBaseContextAttached(base);// you must install multiDex whatever tinker is installed!MultiDex.install(base);// 安装tinker// TinkerManager.installTinker(this); 替换成下面Bugly提供的方法Beta.installTinker(this); }@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)publicvoidregisterActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks){ getApplication().registerActivityLifecycleCallbacks(callbacks); }
③ 将启动Application改为SampleApplication
manifest.png
当enableProxyApplication为true时
① 自定义自己的Application为MyApplication,将所有要初始化的操作都写在MyApplication中,当然为了以防万一将初始化操作做到极致,将补丁安装和多dex在attachBaseContext方法中就开始初始化
@OverrideprotectedvoidattachBaseContext(Context base){super.attachBaseContext(base);// you must install multiDex whatever tinker is installed!MultiDex.install(base);// 安装tinker// TinkerManager.installTinker(this); 替换成下面Bugly提供的方法Beta.installTinker(this); }
② 在onCreate()方法中可初始化设置Bugly一些鬼
这里额外说下Application启动时流程: attachBaseContext(Context) -----> onCreate()
③ 'Applaction'启动项改回来,改为我们自定义的
manifest2.png
到这里也就接近完了,现在看看怎么创建基准包和补丁包
如果我们现在要上线一个App为了以防万一的突发bug要创建一个基准包上线,以防有bug即使热修复
① 首先在Module:app/build.gradle中引入我们为热修复配置好的脚本插件
依赖插件脚本.png
② 现在就可以按照下图生成基准包了,图中1生成的为没有渠道的,2很明显是一个渠道,为腾讯应用宝的渠道,根据自己情况生成基准包
基准包生成.png
看看生成后的目录,其中下面框中的三个中的两个在上传基准包和生成补丁包时要用到,上传在后面,先把打补丁说了
基准包.png
如果我们的App发现了bug我们现在需要打补丁
补丁包1.png
补丁包2.png
看看打补丁后生成的目录,最后生成的7zip.apk就是要上传的补丁包
补丁包.png
基准包、mapping以及补丁包的上传
进入官网登陆,没有项目的可以新建
新建.png
复制App ID在我们的项目中配置
APPID.png
后面的直接上图了:
我们生成的基准包
启动
补丁的发布
image.png
如果你的应用再次全量更新,可以撤回和停止补丁的下发
image.png
mapping的上传
为什么要上传mapping?
mapping是你在项目中配置了混淆时才会有的东西,主要是为了对比编译前后方法名等一些量的混淆,能更方便的调试代码,定位错误位置
image.png
最后贴出以本Demo为例的源码
MyApplication
publicclassMyApplicationextendsApplication{@OverrideprotectedvoidattachBaseContext(Context base){ super.attachBaseContext(base);// you must install multiDex whatever tinker is installed!MultiDex.install(base);// 安装tinker// TinkerManager.installTinker(this); 替换成下面Bugly提供的方法Beta.installTinker(this); } @OverridepublicvoidonCreate(){ super.onCreate(); setBugly(); }privatevoidsetBugly(){/***** Beta高级设置 *****//**
* true表示app启动自动初始化升级模块; false不会自动初始化;
* 开发者如果担心sdk初始化影响app启动速度,可以设置为false,
* 在后面某个时刻手动调用Beta.init(getApplicationContext(),false);
*/Beta.autoInit =true;/**
* true表示初始化时自动检查升级; false表示不会自动检查升级,需要手动调用Beta.checkUpgrade()方法;
*/Beta.autoCheckUpgrade =true;/**
* 设置升级检查周期为60s(默认检查周期为0s),60s内SDK不重复向后台请求策略);
*/Beta.upgradeCheckPeriod =60*1000;/**
* 设置启动延时为1s(默认延时3s),APP启动1s后初始化SDK,避免影响APP启动速度;
*/Beta.initDelay =1*1000;/**
* 设置通知栏大图标,largeIconId为项目中的图片资源;
*/Beta.largeIconId = R.mipmap.ic_launcher;/**
* 设置状态栏小图标,smallIconId为项目中的图片资源Id;
*/Beta.smallIconId = R.mipmap.ic_launcher;/**
* 设置更新弹窗默认展示的banner,defaultBannerId为项目中的图片资源Id;
* 当后台配置的banner拉取失败时显示此banner,默认不设置则展示“loading“;
*/Beta.defaultBannerId = R.mipmap.ic_launcher;/**
* 设置sd卡的Download为更新资源保存目录;
* 后续更新资源会保存在此目录,需要在manifest中添加WRITE_EXTERNAL_STORAGE权限;
*/Beta.storageDir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);/**
* 已经确认过的弹窗在APP下次启动自动检查更新时会再次显示;
*/Beta.showInterruptedStrategy =true;/**
* 只允许在MainActivity上显示更新弹窗,其他activity上不显示弹窗; 不设置会默认所有activity都可以显示弹窗;
*/Beta.canShowUpgradeActs.add(MainActivity.class);/***** Bugly高级设置 *****/BuglyStrategy strategy =newBuglyStrategy();/**
* 设置app渠道号
*/strategy.setAppChannel(ChannelUtils.getAppMetaData(this,"UMENG_CHANNEL"));// /**// * 单个异常上传统计时的初始化方法// * @appContext 上下文// * @crashReportAppId 注册时申请的APPID// * @isDebug 自定义日志将会在Logcat中输出。// */// CrashReport.initCrashReport(getApplicationContext(), Commont.APP_ID, false);//// /**// * 统一初始化Bugly产品,包含Beta// * @context 上下文// * @appId 注册时申请的APPID// * @isDebug 自定义日志将会在Logcat中输出。// *// * 提示:已经接入Bugly用户改用上面的初始化方法,不影响原有的crash上报功能; init方法会自动检测更新,不需要再手动调用Beta.checkUpgrade(), 如需增加自动检查时机可以使用Beta.checkUpgrade(false,false);// * 参数1:isManual 用户手动点击检查,非用户点击操作请传false// * 参数2:isSilence 是否显示弹窗等交互,[true:没有弹窗和toast] [false:有弹窗或toast]// */// Bugly.init(this, Commont.APP_ID, true);/**
* 统一初始化Bugly产品,包含Beta
* @context 上下文
* @appId 注册时申请的APPID
* @isDebug 自定义日志将会在Logcat中输出。
* @strategy Bugly高级设置
*/Bugly.init(this, Commont.APP_ID,true, strategy); }}
SampleApplication
/**
* 注意:这个类集成TinkerApplication类,这里面不做任何操作,所有Application的代码都会放到ApplicationLike继承类当中
* 参数解析
* 参数1:tinkerFlags 表示Tinker支持的类型 dex only、library only or all suuport,default: TINKER_ENABLE_ALL
* 参数2:delegateClassName Application代理类 这里填写你自定义的ApplicationLike
* 参数3:loaderClassName Tinker的加载器,使用默认即可
* 参数4:tinkerLoadVerifyFlag 加载dex或者lib是否验证md5,默认为false
*/publicclassSampleApplicationextendsTinkerApplication{publicSampleApplication(){//将‘com.suchengkeji.android.bugly.applications.SampleApplicationLike’替换为自己的包名及路径super(ShareConstants.TINKER_ENABLE_ALL,"com.suchengkeji.android.bugly.applications.SampleApplicationLike","com.tencent.tinker.loader.TinkerLoader",false); }}
SampleApplicationLike
publicclassSampleApplicationLikeextendsDefaultApplicationLike{publicSampleApplicationLike(Application application,inttinkerFlags, boolean tinkerLoadVerifyFlag,longapplicationStartElapsedTime,longapplicationStartMillisTime, Intent tinkerResultIntent){ super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent); } @OverridepublicvoidonCreate(){ super.onCreate();// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId// 调试时,将第三个参数改为true// Bugly.init(getApplication(), APP_ID, false);setBUG(); }privatevoidsetBUG(){/***** Beta高级设置 *****//**
* true表示app启动自动初始化升级模块; false不会自动初始化;
* 开发者如果担心sdk初始化影响app启动速度,可以设置为false,
* 在后面某个时刻手动调用Beta.init(getApplicationContext(),false);
*/Beta.autoInit =true;/**
* true表示初始化时自动检查升级; false表示不会自动检查升级,需要手动调用Beta.checkUpgrade()方法;
*/Beta.autoCheckUpgrade =true;/**
* 设置升级检查周期为60s(默认检查周期为0s),60s内SDK不重复向后台请求策略);
*/Beta.upgradeCheckPeriod =60*1000;/**
* 设置启动延时为1s(默认延时3s),APP启动1s后初始化SDK,避免影响APP启动速度;
*/Beta.initDelay =1*1000;/**
* 设置通知栏大图标,largeIconId为项目中的图片资源;
*/Beta.largeIconId = R.mipmap.ic_launcher;/**
* 设置状态栏小图标,smallIconId为项目中的图片资源Id;
*/Beta.smallIconId = R.mipmap.ic_launcher;/**
* 设置更新弹窗默认展示的banner,defaultBannerId为项目中的图片资源Id;
* 当后台配置的banner拉取失败时显示此banner,默认不设置则展示“loading“;
*/Beta.defaultBannerId = R.mipmap.ic_launcher;/**
* 设置sd卡的Download为更新资源保存目录;
* 后续更新资源会保存在此目录,需要在manifest中添加WRITE_EXTERNAL_STORAGE权限;
*/Beta.storageDir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);/**
* 已经确认过的弹窗在APP下次启动自动检查更新时会再次显示;
*/Beta.showInterruptedStrategy =true;/**
* 只允许在MainActivity上显示更新弹窗,其他activity上不显示弹窗; 不设置会默认所有activity都可以显示弹窗;
*/Beta.canShowUpgradeActs.add(MainActivity.class);/***** Bugly高级设置 *****/BuglyStrategy strategy =newBuglyStrategy();/**
* 设置app渠道号
* ChannelUtils.getAppMetaData(this, "UMENG_CHANNEL")获取渠道号
*/strategy.setAppChannel(ChannelUtils.getAppMetaData(getApplication(),"UMENG_CHANNEL"));/***** 统一初始化Bugly产品,包含Beta *****/Bugly.init(getApplication(), Commont.APP_ID,true, strategy); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @OverridepublicvoidonBaseContextAttached(Context base){ super.onBaseContextAttached(base);// you must install multiDex whatever tinker is installed!MultiDex.install(base);// 安装tinker// TinkerManager.installTinker(this); 替换成下面Bugly提供的方法Beta.installTinker(this); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)publicvoidregisterActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks){ getApplication().registerActivityLifecycleCallbacks(callbacks); }}
MainActivity
publicclassMainActivityextendsAppCompatActivityimplementsAdapterView.OnItemClickListener{@BindView(R.id.list_view) ListView listView; String[] strings = {"异常上报","应用更新","热修复测试"};@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); setList(); }privatevoidsetList(){ listView.setAdapter(newBaseAdapter() {@OverridepublicintgetCount(){returnstrings.length; }@OverridepublicObjectgetItem(intposition){returnstrings[position]; }@OverridepubliclonggetItemId(intposition){returnposition; }@OverridepublicViewgetView(intposition, View convertView, ViewGroup parent){@SuppressLint("ViewHolder") View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.list_item, parent,false); TextView textView = (TextView) view.findViewById(R.id.list_item_text); textView.setText(strings[position]);returnview; } }); listView.setOnItemClickListener(this); }@OverridepublicvoidonItemClick(AdapterView parent, View view,intposition,longid){ Log.d(LogUtils.TAG(), position +"");switch(position) {case0:/***** 异常崩溃上传 *****/CrashReport.testJavaCrash();break;case1:/***** 检查更新 *****/loadUpgradeInfo(); loadAppInfo(); Beta.checkUpgrade();break;case2:/***** 崩溃上传(此处要打修复补丁) *****/FixBugClass.getTimer(MainActivity.this);// BugClass.getTimer(MainActivity.this);break; } }@OverrideprotectedvoidonStart(){super.onStart();//添加权限PermissionGen.with(this) .addRequestCode(Commont.PHOTO_PERMISS) .permissions( Manifest.permission.INTERNET, Manifest.permission.READ_PHONE_STATE, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.ACCESS_WIFI_STATE, Manifest.permission.READ_LOGS, Manifest.permission.WRITE_EXTERNAL_STORAGE) .request(); }@PermissionSuccess(requestCode = Commont.PHOTO_PERMISS)publicvoidrequestPhotoSuccess(){//成功之后的处理//.......Log.d(LogUtils.TAG(),"动态权限加载成功"); }@PermissionFail(requestCode = Commont.PHOTO_PERMISS)publicvoidrequestPhotoFail(){//失败之后的处理,我一般是跳到设置界面Log.d(LogUtils.TAG(),"动态权限加载失败"); }/**
* 获取升级信息
*/privatevoidloadUpgradeInfo(){/***** 获取升级信息 *****/UpgradeInfo upgradeInfo = Beta.getUpgradeInfo();if(upgradeInfo ==null) { Log.d(LogUtils.TAG(),"无升级信息");return; } StringBuilder info =newStringBuilder(); info.append("id: ").append(upgradeInfo.id).append("\n"); info.append("标题: ").append(upgradeInfo.title).append("\n"); info.append("升级说明: ").append(upgradeInfo.newFeature).append("\n"); info.append("versionCode: ").append(upgradeInfo.versionCode).append("\n"); info.append("versionName: ").append(upgradeInfo.versionName).append("\n"); info.append("发布时间: ").append(upgradeInfo.publishTime).append("\n"); info.append("安装包Md5: ").append(upgradeInfo.apkMd5).append("\n"); info.append("安装包下载地址: ").append(upgradeInfo.apkUrl).append("\n"); info.append("安装包大小: ").append(upgradeInfo.fileSize).append("\n"); info.append("弹窗间隔(ms): ").append(upgradeInfo.popInterval).append("\n"); info.append("弹窗次数: ").append(upgradeInfo.popTimes).append("\n"); info.append("发布类型(0:测试 1:正式): ").append(upgradeInfo.publishType).append("\n"); info.append("弹窗类型(1:建议 2:强制 3:手工): ").append(upgradeInfo.upgradeType); Log.d(LogUtils.TAG(), info +""); }privatevoidloadAppInfo(){try{ StringBuilder info =newStringBuilder(); PackageInfo packageInfo =this.getPackageManager().getPackageInfo(this.getPackageName(), PackageManager.GET_CONFIGURATIONS);intversionCode = packageInfo.versionCode; String versionName = packageInfo.versionName; info.append("appid: ").append(Commont.APP_ID).append(" ") .append("channel: ") .append(ChannelUtils.getAppMetaData(MainActivity.this,"UMENG_CHANNEL")) .append(" ") .append("version: ").append(versionName).append(".").append(versionCode); Log.d(LogUtils.TAG(), info +""); }catch(Exception e) { e.printStackTrace(); } }}
Commont
publicclassCommont{/*** 腾讯Bugly产品ID ***/publicstaticfinalString APP_ID ="5e****70c08";/*** 权限处理标识 ***/publicstaticfinalint PHOTO_PERMISS =100;}
ChannelUtils
/**
* 获取app当前的渠道号或application中指定的meta-data
*
* @return 如果没有获取成功(没有对应值,或者异常),则返回值为空
*/publicstaticStringgetAppMetaData(Context context,Stringkey) {if(context ==null|| TextUtils.isEmpty(key)) {returnnull; }StringchannelNumber =null;try{ PackageManager packageManager = context.getPackageManager();if(packageManager !=null) { ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);if(applicationInfo !=null) {if(applicationInfo.metaData !=null) { channelNumber = applicationInfo.metaData.getString(key); } } } }catch(PackageManager.NameNotFoundException e) { e.printStackTrace(); }returnchannelNumber; }// 在需要的地方调用// String channelNumber = getAppMetaData(getBaseContext(), "UMENG_CHANNEL");//获取app当前的渠道号}
BugClass
/**
* 有错误的bug类
*/publicclassBugClass{privatestaticString timeType ="yyyy-MM-dd HH:mm:ss";publicstaticvoidgetTimer(Context c){ String date =null; Log.d(LogUtils.TAG(),"上线后的紧急BUG"); Log.d(LogUtils.TAG(), date.length() +"");//此处留一个空指针bug@SuppressLint("SimpleDateFormat") SimpleDateFormat sDateFormat =newSimpleDateFormat(timeType); date = sDateFormat.format(newjava.util.Date()); Toast.makeText(c, date, Toast.LENGTH_SHORT).show(); }}
FixBugClass
/**
* 修复过bug的补丁类
*/publicclassFixBugClass{privatestaticString timeType ="yyyy-MM-dd HH:mm:ss";publicstaticvoidgetTimer(Context c){ String date =null; Log.d(LogUtils.TAG(),"上线后的紧急BUG修复补丁");//Log.d(LogUtils.TAG(), date.length() + "");//去掉此处的bug@SuppressLint("SimpleDateFormat") SimpleDateFormat sDateFormat =newSimpleDateFormat(timeType); date = sDateFormat.format(newjava.util.Date()); Toast.makeText(c, date, Toast.LENGTH_SHORT).show(); }}
注意点,打完补丁之后在运行应用是,第一次仍然会崩溃,不过在崩溃时会自己打补丁修复,第二次以后就好了
好了,到这里就完了,此文章仅供参考,具体以官方文档为准,有更好的思路或方法,请不吝告知,如有错误,请及时指出。。
作者:一s独秀
链接:https://www.jianshu.com/p/fbab774dab8e
来源:
著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。