switch (view.getId()){
case R.id.mCreateBug: //生成Bug
Utils.printLog();
break;
}
}
然后在Utils类中的**printLog()**方法中给它制造Crash,这里就简单的让它产生空指针异常:
//构造异常方法
public static void printLog() {
String info = null;
Log.e(“jarchie-andfix”, info);
}
④、Build异常APK
在构建APK时,我们需要构建带签名的版本,这是为了接下来生成apatch文件用的。关于如何构建release版本的APK我就不多说了,相信没有不知道的吧,构建完了之后,你可以把它弄到你的手机上,通过adb push或者文件传输工具都可以,只要安装到你手机上就行,注意这里需要将这个有bug的apk保存一份,因为后面要用到。
2.3.2、构建正常APK
①、修改空指针异常
public static void printLog() {
String info = “Jarchie”; //修复空指针
Log.e(“jarchie-andfix”, info);
}
②、构建修复后的APK
将修改后的代码重新打包,生成新的release包,这里也将新的apk包保存一份。
2.4、修复BUG
①、生成apatch文件
生成apatch文件主要是用到了apkpatch这个命令行工具,这个工具包在github上有,大家下载到自己电脑上就行了:
里面就3个文件,windows用户使用.bat的这个,Linux或者MAC OS的用户使用.sh的这个。
然后我将之前Build的两个apk和jks文件都复制到这个文件夹中,并且新建了一个文件夹outputs作为apatch文件的输出目录:
然后打开控制台,进入到apkpatch这个目录下,执行apkpatch命令来看一下这个命令的用法介绍:
上面的是用来生成apatch文件,下面的是用来合并多个patch文件为一个的时候用的,具体的参数下面也都给出了,并且也都有注释说明(虽然都是英文,但相信你都能看的懂)。
然后我们就来使用apkpatch命令来生成我们的.apatch差异包:
执行完这个命令就生成了我们的差异包,并且它还会告诉你哪个类的哪个方法做了修改,正好就是我们的printLog()方法修改了。
进到本地目录中可以看到确实生成了apatch文件,我将它重命名为 jaqandfix.apatch。
②、push apatch文件
在生成了apatch文件之后,就可以将它放到手机对应的目录中,这一步操作同样也没有限制具体的方法,你可以通过文件传输工具,也可以直接通过adb命令将文件push到对应的目录,我这里使用adb命令的方式进行:
可以看到,我们手机中对应的目录下面已经有了push进来的jaqandfix.apatch文件。
③、修复BUG
再次进入App,然后首先点击修复BUG,它会去load这个补丁文件,当你再次点击产生BUG时,你会发现BUG已经被修复了。
注意:官网上给出的是2.1-7.0的版本,如果你各种操作步骤都是正确的,但是没有效果,那就换一台手机试一下,因为毕竟这个东西并不是所有机型都适配的,这里主要是学习它的方法。还有一点是,实际应用中,补丁文件是肯定不可能通过adb push这种方式进入用户手机中的,基本上都是通过服务端下发,客户端是一个下载文件的过程,这一点也需要注意。
到这里就已经说完了AndFix的修复流程,整个流程总结下来就是下面这张简化的图:
三、AndFix源码解析
============
首先找到之前封装的AndFixPatchManager类,然后找到initPatch()方法:
//初始化AndFix方法
public void initPatch(Context context){
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getVersionName(context));
mPatchManager.loadPatch();
}
从代码中可以看到,所有的操作都是通过AndFix的PatchManager类来完成的,很明显是外观模式,将所有的API都包含在了PatchManger中,所以不需要关注AndFix其他模块的作用。这里需要说明一点,阅读源码我们不可能把每一个类的每一行代码都完全弄懂,我们读源码是为了了解这个框架的实现过程,所以最好的方式就是结合在应用层我们自己的业务代码中调用它的那些类和方法,按照顺序一一跟进阅读,把整个调用流程串起来就OK了。
好,现在来打开PatchManager类,首先看一下它里面几个比较重要的成员变量:
/**
- context
*/
private final Context mContext;
/**
- AndFix manager
*/
private final AndFixManager mAndFixManager;
/**
- patch directory
*/
private final File mPatchDir;
/**
- patchs
*/
private final SortedSet mPatchs;
/**
- classloaders
*/
private final Map<String, ClassLoader> mLoaders;
-
AndFixManager:所有的方法替换、BUG修复都是由AndFixManager来完成的
-
SortedSet:经过排序后的Set集合,包含应用所有的Patch文件
接着来看一下它的构造方法,因为我们在应用层最先调用的就是它的构造方法:
/**
-
@param context
-
context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);
mPatchDir = new File(mContext.getFilesDir(), DIR);
mPatchs = new ConcurrentSkipListSet();
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
可以看到构造方法主要就是进行了一系列的初始化:上下文、AndFixManager、文件夹、数据结构等等的初始化操作。
接着来看我们应用层调用的第一个方法init()方法:
/**
-
initialize
-
@param appVersion
-
App version
*/
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, “patch dir create error.”);
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
initPatchs();
}
}
入参是需要传入当前应用的版本号,然后内部一开始是进行了文件夹的判断,满足了条件之后,它会从AndFix的SharedPreferences中拿到之前保存的版本号,然后通过这个版本号和入参中传入的版本号去做一个判断,如果不同,表明我们的应用已经做了升级,然后就会调用cleanPatch()去删除所有的Patch文件,同时更新版本号,用于下一次的比较,如果版本号相同,表明没有升级,则会调用**initPatchs()**方法,接下来,跟进这个initPatchs()方法:
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
这个方法很简单,就是遍历指定Patch文件夹下的所有文件,然后将它们通过_addPatch()_方法添加到mPatchs这个PatchList中,跟进addPatch()方法看一下:
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);
mPatchs.add(patch);
} catch (IOException e) {
Log.e(TAG, “addPatch”, e);
}
}
return patch;
}
这个方法内部首先是判断传入的文件后缀名是否符合.apatch格式,如果符合,将其转化为Patch文件,然后将文件添加到PatchList中,所以这里的mPatchs内部就是保存了所有的Patch文件。然后点击Patch类进入到这个类中看一下它是如何将一个File转化为Patch类的?这个Patch类就相当于是一个实体类,这个类中定义了一些成员变量:
/**
- patch file
*/
private final File mFile;
/**
- name
*/
private String mName;
/**
- create time
*/
private Date mTime;
/**
- classes of patch
*/
private Map<String, List> mClassesMap;
主要有传入的文件、文件名、mClassMap等,mClassMap是存储了本次Patch文件所有要修复的class的字符串,然后会调用类中的init()方法完成解析:
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);
mTime = new Date(main.getValue(CREATED_TIME));
mClassesMap = new HashMap<String, List>();
Attributes.Name attrName;
String name;
List strings;
for (Iterator<?> it = main.keySet().iterator(); it.hasNext()😉 {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(“,”));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// “-Classes”
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
这个方法是首先把文件转化成jar文件,然后解析jar文件中的所有字段比如:PATCH_NAME、CREATED_TIME等,这些字段是我们之前通过apatch命令行工具生成apatch文件的时候添加的,所以在这里可以直接解析了。然后来说mClassMap是如何初始化的,它会找到所有的Class,然后判断一下是不是自己要解析的PATCH_CLASS,如果是就添加到以当前Patch文件名为key的Map中,添加进来之后当你后续使用的时候,就可以直接通过getClasses传入当前的Patch文件名获取这个Patch文件中所有要修复的Class的绝对路径:
public List getClasses(String patchName) {
return mClassesMap.get(patchName);
}
现在我们应该清楚了这个Patch文件的作用了,它就是将普通磁盘上的File转化成PatchFile方便使用。OK,到这里这个PatchManager的init()方法就说完了,总结一下它的作用就是对Patch文件的删除和添加。
应用层中在我们下载完Patch文件之后,我们调用了addPatch()方法还记得吗?mPatchManager.addPatch(path); 现在就来看一下这个addPatch()方法是如何实现的?
/**
-
add patch at runtime
-
@param path
-
patch path
-
@throws IOException
*/
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if(!src.exists()){
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, “patch [” + path + “] has be loaded.”);
return;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
学习分享
在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了
很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘
如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。
2021最新上万页的大厂面试真题
七大模块学习资料:如NDK模块开发、Android框架体系架构…
只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。
这份体系学习笔记,适应人群:
**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。
**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
P-1712527187341)]
只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。
这份体系学习笔记,适应人群:
**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。
**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算