蜂信物联FastBee平台https://gitee.com/beecue/fastbee
阿里资料开源项目https://gitee.com/vip204888
百度低代码前端框架https://gitee.com/baidu/amis
OpenHarmony开源项目https://gitcode.com/openharmony
仓颉编程语言开放项目https://gitcode.com/Cangjie
-keepattributes Signature
抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
指定混淆是采用的算法,后面的参数是一个过滤器
这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/,!class/merging/
#############################################
Android开发中一些需要保留的公共部分
#############################################
保留我们使用的四大组件,自定义的 Application 等等这些类不被混淆
因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
保留 support 下的所有类及其内部类
-keep class android.support.** { *; }
保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
保留 R 下面的资源
-keep class *.R$ { *; }
保留本地 native 方法不被混淆
-keepclasseswithmembernames class * {
native ;
}
保留在 Activity 中的方法参数是view的方法,
这样以来我们在 layout 中写的 onClick 就不会被影响
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
保留我们自定义控件(继承自 View)不被混淆
-keep public class * extends android.view.View {
*** get*();
void set*(***);
public (android.content.Context);
public (android.content.Context, android.util.AttributeSet);
public (android.content.Context, android.util.AttributeSet, int);
}
保留 Parcelable 序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
保留 Serializable 序列化的类不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient ;
!private ;
!private ;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
对于带有回调函数的 onXXEvent、**On*Listener 的,不能被混淆
-keepclassmembers class * {
void (**OnEvent);
void (**OnListener);
}
webView 处理,项目中没有使用到 webView 忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, java.lang.String);
}
js
-keepattributes JavascriptInterface
-keep class android.webkit.JavascriptInterface { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface ;
}
@Keep
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}
如果是aar这种插件,可以在aar的build.gralde中添加如下混淆配置。
android {
···
defaultConfig {
···
consumerProguardFile ‘proguard-rules.pro’
}
···
}
5,NDK
如果要问Android的高级开发知识,那么NDK肯定是必问的。那么什么的NDK,NDK 全称是 Native Development Kit,是一组可以让开发者在 Android 应用中使用C/C++ 的工具。通常,NDK可以用在如下的场景中:
-
从设备获取更好的性能以用于计算密集型应用,例如游戏或物理模拟。
-
重复使用自己或其他开发者的 C/C++ 库,便利于跨平台。
-
NDK 集成了譬如 OpenSL、Vulkan 等 API 规范的特定实现,以实现在 Java 层无法做到的功能,如音视频开发、渲染。
-
增加反编译难度。
5.1, JNI基础
JNI即java native interface,是Java和Native代码进行交互的接口。
5.1.1 JNI 访问 Java 对象方法
假如,有如下一个Java类,代码如下。
package com.xzh.jni;
public class MyJob {
public static String JOB_STRING = “my_job”;
private int jobId;
public MyJob(int jobId) {
this.jobId = jobId;
}
public int getJobId() {
return jobId;
}
}
然后,在cpp目录下,新建native_lib.cpp,添加对应的native实现。
#include <jni.h>
extern “C”
JNIEXPORT jint JNICALL
Java_com_xzh_jni_MainActivity_getJobId(JNIEnv *env, jobject thiz, jobject job) {
// 根据实例获取 class 对象
jclass jobClz = env->GetObjectClass(job);
// 根据类名获取 class 对象
jclass jobClz = env->FindClass(“com/xzh/jni/MyJob”);
// 获取属性 id
jfieldID fieldId = env->GetFieldID(jobClz, “jobId”, “I”);
// 获取静态属性 id
jfieldID sFieldId = env->GetStaticFieldID(jobClz, “JOB_STRING”, “Ljava/lang/String;”);
// 获取方法 id
jmethodID methodId = env->GetMethodID(jobClz, “getJobId”, “()I”);
// 获取构造方法 id
jmethodID initMethodId = env->GetMethodID(jobClz, “”, “(I)V”);
// 根据对象属性 id 获取该属性值
jint id = env->GetIntField(job, fieldId);
// 根据对象方法 id 调用该方法
jint id = env->CallIntMethod(job, methodId);
// 创建新的对象
jobject newJob = env->NewObject(jobClz, initMethodId, 10);
return id;
}
5.2 NDK开发
5.2.1 基本流程
首先,在 Java代码中声明 Native 方法,如下所示。
public class MainActivity extends AppCompatActivity {
// Used to load the ‘native-lib’ library on application startup.
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(“MainActivity”, stringFromJNI());
}
private native String stringFromJNI();
}
然后,新建一个 cpp 目录,并且新建一个名为native-lib.cpp的cpp 文件,实现相关方法。
#include <jni.h>
extern “C” JNIEXPORT jstring JNICALL
Java_com_xzh_jni_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}
cpp文件遵循如下的规则:
-
函数名的格式遵循遵循如下规则:Java_包名_类名_方法名。
-
extern “C” 指定采用 C 语言的命名风格来编译,否则由于 C 与 C++ 风格不同,导致链接时无法找到具体的函数
-
JNIEnv*:表示一个指向 JNI 环境的指针,可以通过他来访问 JNI 提供的接口方法
-
jobject:表示 java 对象中的 this
-
JNIEXPORT 和 JNICALL:JNI 所定义的宏,可以在 jni.h 头文件中查找到
System.loadLibrary()的代码位于java/lang/System.java文件中,源码如下:
@CallerSensitive
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
5.3 CMake 构建 NDK
CMake 是一个开源的跨平台工具系列,旨在构建、测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地使用 CMake 与 Gradle 搭配使用来构建原生库。具体来说,我们可以使用 Gradle 将 C \ C++ 代码 编译到原生库中,然后将这些代码打包到我们的应用中, Java 代码随后可以通过 Java 原生接口 ( JNI ) 调用 我们原生库中的函数。
使用CMake开发NDK项目需要下载如下一些套件:
-
Android 原生开发工具包 (NDK):这套工具集允许我们 开发 Android 使用 C 和 C++ 代码,并提供众多平台库,让我们可以管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。
-
CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果你只计划使用 ndk-build,则不需要此组件。
-
LLDB:一种调试程序,Android Studio 使用它来调试原生代码。
我们可以打开Android Studio,依次选择 【Tools】 > 【Android】> 【SDK Manager】> 【SDK Tools】选中LLDB、CMake 和 NDK即可。
启用CMake还需要在 app/build.gradle 中添加如下代码。
android {
···
defaultConfig {
···
externalNativeBuild {
cmake {
cppFlags “”
}
}
ndk {
abiFilters ‘arm64-v8a’, ‘armeabi-v7a’
}
}
···
externalNativeBuild {
cmake {
path “CMakeLists.txt”
}
}
}
然后,在对应目录新建一个 CMakeLists.txt 文件,添加代码。
定义了所需 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)
add_library() 命令用来添加库
native-lib 对应着生成的库的名字
SHARED 代表为分享库
src/main/cpp/native-lib.cpp 则是指明了源文件的路径。
add_library( # Sets the name of the library.
native-lib
Sets the library as a shared library.
SHARED
Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)
find_library 命令添加到 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。
可以使用此变量在构建脚本的其他部分引用 NDK 库
find_library( # Sets the name of the path variable.
log-lib
Specifies the name of the NDK library that
you want CMake to locate.
log)
预构建的 NDK 库已经存在于 Android 平台上,因此,无需再构建或将其打包到 APK 中。
由于 NDK 库已经是 CMake 搜索路径的一部分,只需要向 CMake 提供希望使用的库的名称,并将其关联到自己的原生库中
要将预构建库关联到自己的原生库
target_link_libraries( # Specifies the target library.
native-lib
Links the target library to the log library
included in the NDK.
${log-lib})
···
6,动态加载
6.1 基本概念
动态加载技术在Web中很常见,对于Android项目来说,动态加载的目的是让用户不用重新安装APK就能升级应用的功能,主要的应用场景是插件化和热修复。
首先需要明确的一点,插件化和热修复不是同一个概念,虽然站在技术实现的角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;但是二者的出发点是不同的。
插件化,本质上是把需要实现的模块或功能当做一个独立的功能提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。而热修复则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。
为了方便说明,我们先理清几个概念:
-
宿主: 当前运行的APP。
-
插件: 相对于插件化技术来说,就是要加载运行的apk类文件。
-
补丁: 相对于热修复技术来说,就是要加载运行的.patch,.dex,*.apk等一系列包含dex修复内容的文件。
下图展示了Android动态化开发框架的整体的架构。
6.2 插件化
关于插件化技术,最早可以追溯到2012年的 AndroidDynamicLoader ,其原理是动态加载不同的Fragment实现UI替换,不过随着15,16年更好的方案,这个方案渐渐的被淘汰了。后面的方案大多基于Hook和动态代理两个方向进行。
目前,插件化的开发并没有一个官方的插件化方案,它是国内提出的一种技术实现,利用虚拟机的类的加载机制实现的一种技术手段,往往需要hook一些系统api,而Google从Android9.0开始限制对系统私有api的使用,也就造成了插件化的兼容性问题,现在几个流行的插件化技术框架,都是大厂根据自己的需求,开源出来的,如滴滴的VirtualAPK,360的RePlugin等,大家可以根据需要自行了解技术的实现原理。
6.3 热修复
6.3.1 热修复原理
说到热修复的原理,就不得不提到类的加载机制,和常规的JVM类似,在Android中类的加载也是通过ClassLoader来完成,具体来说就是PathClassLoader 和 DexClassLoader 这两个Android专用的类加载器,这两个类的区别如下。
-
PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
-
DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是我们一开始提到的补丁。
这两个类都是继承自BaseDexClassLoader,BaseDexClassLoader的构造函数如下。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
这个构造函数只做了一件事,就是通过传递进来的相关参数,初始化了一个DexPathList对象。DexPathList的构造函数,就是将参数中传递进来的程序文件(就是补丁文件)封装成Element对象,并将这些对象添加到一个Element的数组集合dexElements中去。
前面说过类加载器的作用,就是将一个具体的类(class)加载到内存中,而这些操作是由虚拟机完成的,对于开发者来说,只需要关注如何去找到这个需要加载的类即可,这也是热修复需要干的事情。
在Android中,查找一个名为name的class需要经历如下两步:
-
在DexClassLoader的findClass 方法中通过一个DexPathList对象findClass()方法来获取class。
-
在DexPathList的findClass 方法中,对之前构造好dexElements数组集合进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
因此,基于上面的理论,我们可以想到一个最简单的热修复方案。假设现在代码中的某一个类出现Bug,那么我们可以在修复Bug之后,将这些个类打包成一个补丁文件,然后通过这个补丁文件封装出一个Element对象,并且将这个Element对象插到原有dexElements数组的最前端。这样,当DexClassLoader去加载类时,由于双亲加载机制的特点,就会优先加载插入的这个Element,而有缺陷的Element则没有机会再被加载。事实上,QQ早期的热修复方案就是这样的。
6.3.2 QQ 空间超级补丁方案
QQ 空间补丁方案就是使用javaassist 插桩的方式解决了CLASS_ISPREVERIFIED的难题。涉及的步骤如下:
-
在apk安装的时候系统会将dex文件优化成odex文件,在优化的过程中会涉及一个预校验的过程。
-
如果一个类的static方法,private方法,override方法以及构造函数中引用了其他类,而且这些类都属于同一个dex文件,此时该类就会被打上CLASS_ISPREVERIFIED。
-
如果在运行时被打上CLASS_ISPREVERIFIED的类引用了其他dex的类,就会报错。
-
正常的分包方案会保证相关类被打入同一个dex文件。
-
想要使得patch可以被正常加载,就必须保证类不会被打上CLASS_ISPREVERIFIED标记。而要实现这个目的就必须要在分完包后的class中植入对其他dex文件中类的引用。
6.3.3 Tinker
QQ空间超级补丁方案在遇到补丁文件很大的时候耗时是非常严重的,因为一个大文件夹加载到内存中构建一个Element对象时,插入到数组最前端是需要耗费时间的,而这非常影响应用的启动速度。基于这些问题,微信提出了Tinker 方案。
Tinker的思路是,通过修复好的class.dex 和原有的class.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 整体替换原有的dexPathList的中的内容,进而从根本上修复Bug,下图是演示图。
相比QQ空间超级补丁方案,Tinker 提供的思路可以说效率更高。
6.3.4 HotFix
以上提到的两种方式,虽然策略有所不同,但总的来说都是从上层ClassLoader的角度出发,由于ClassLoader的特点,如果想要新的补丁文件再次生效,无论你是插桩还是提前合并,都需要重新启动应用来加载新的DexPathList,从而实现Bug的修复。
AndFix 提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。不过,由于Android在国内变成了安卓,各大手机厂商定制了自己的ROM,所以很多底层实现的差异,导致AndFix的兼容性并不是很好。
题外话
不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊
这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
欢迎评论区讨论。
.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 整体替换原有的dexPathList的中的内容,进而从根本上修复Bug,下图是演示图。
相比QQ空间超级补丁方案,Tinker 提供的思路可以说效率更高。
6.3.4 HotFix
以上提到的两种方式,虽然策略有所不同,但总的来说都是从上层ClassLoader的角度出发,由于ClassLoader的特点,如果想要新的补丁文件再次生效,无论你是插桩还是提前合并,都需要重新启动应用来加载新的DexPathList,从而实现Bug的修复。
AndFix 提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。不过,由于Android在国内变成了安卓,各大手机厂商定制了自己的ROM,所以很多底层实现的差异,导致AndFix的兼容性并不是很好。
题外话
不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊
这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
[外链图片转存中…(img-A6Oktm7k-1725119726172)]
欢迎评论区讨论。