admin管理员组

文章数量:1651798

一, Gradle 、AGP(Android Gradle Plugin)、 buildTools分别是什么,他们之间什么关系?

  1. Gradle
    Gradle是基于JVM的构建工具。他本身使用jave写的,gradle的脚本也就是build.gradle通常是用groovy语言。
  2. Android BuildTools
    Android SDK Build-Tools 是构建 Android apk、AAR文件等Android平台产物所需的一个 Android SDK 组件,安装在 /build-tools/ 目录下。
    ├── ⋮
    ├── aapt2
    ├── d8
    ├── apksigner
    ├── zipalign
    ├── ⋮
  • AAPT2*:Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件使用它来编译和打包应用的。AAPT2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。
  • apksigner:工具为 APK 签名,并确保 APK 的签名将在该 APK 支持的所有版本 Android 平台上成功通过验证。
  • d8:Android Gradle 插件使用该工具来将项目的 Java 字节码编译为在 Android 设备上运行的 DEX 字节码。
  • Zipalign:在将 APK 文件分发给最终用户之前,先使用 zipalign 进行优化。

二,在rootProject下的build.gradle中,buildscript的
repositories和allprojects的repositories有什么区别?

buildscript {
    repositories {
        jcenter()
        google()
        maven {
            url 'https://maven.google/'
            name 'Google'
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
    }
}
allprojects {
    repositories {
        jcenter()
        google()
        maven {
            url "http://maven.xxxxxxxx/xxxxx"
        }
    }
}

• buildscript里是gradle脚本执行所需依赖,如上所示对应的是maven库和插件 。
• allprojects里是项目本身需要的依赖,比如代码中某个类是打包到maven私有库中的,那么在allprojects—>repositories中需要配置maven私有库,而不是buildscript中,不然找不到。

三,Kotlin携程 Dispatchers.Default和Dispatchers.IO的区别

  • Dispatchers.Default 提交任务,此时线程池里所有任务都在忙碌,于是尝试创建新的线程,而又因为当前计算型的线程数=8,等于核心线程数,此时不能创建新的线程,因此该任务暂时无法被线程执行。
  • Dispatchers.IO提交任务,此时线程池里所有任务都在忙碌,于是尝试创建新的线程,而当前阻塞的任务数为1,当前线程池所有线程个数为8,因此计算型的线程数为 8-1=7,小于核心线程数,最后可以创建新的线程用以执行任务。
  • 这也是两者的最大差异,因为对于计算型(非阻塞)的任务,很占CPU,即使分配再多的线程,CPU没有空闲去执行这些线程也是白搭,而对于IO型(阻塞)的任务,不怎么占CPU,因此可以多开几个线程充分利用CPU性能。

四,App抓包和防抓包

使用Charles抓包需要做哪些操作:

  1. 电脑上需要安装证书。这个主要是让Charles充当中间人,颁布自己的CA证书。
  2. 手机上需要安装证书。这个是访问Charles获取手机证书,然后安装即可。
  3. Android项目代码设置兼容。Google 推出更加严格的安全机制,应用默认不信任用户证书(手机里自己安装证书),自己的app可以通过配置解决,相当于信任证书的一种操作!

防抓包的关键点:

  1. APP安全配置
  • 添加配置文件:
    android:networkSecurityConfig=“@xml/network_security_config”
  • 需要给okhttpClient配置:
    用X509TrustManager 来监听校验服务端证书有效性。遍历设备上信任的证书,通过证书别名将用户证书(别名中含有user字段)过滤掉,只将系统证书添加到验证列表中
  1. 关闭代理
  • charles 和 fiddler 都使用代理来进行抓包,对网络客户端使用无代理模式即可防止抓包,如:
    OkHttpClient.Builder()
    .proxy(Proxy.NO_PROXY)
    .build()
  • 通常情况下上述的办法有用,但是无法防住使用 VPN 导流进行的抓包:
    使用VPN抓包的原理是,先将手机请求导到VPN,再对VPN的网络进行Charles的代理,绕过了对App的代理。
  1. 证书校验(单向认证)
  • 下载服务器端公钥证书:
    为了防止上面方案可能导致的“中间人攻击”,可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中一般在assets文件夹保存,由应用在交互过程中去验证证书的合法性。
  • 设置证书校验:
    通过OkHttp的API方法 sslSocketFactory(sslSocketFactory,trustManager) 设置SSL证书校验。
  • 设置域名合法性校验:
    通过OkHttp的API方法 hostnameVerifier(hostnameVerifier) 设置域名合法性校验。
  1. APP安全配置双向认证
  • 什么叫做双向认证:
    SSL/TLS 协议提供了双向认证的功能,即除了 Client 需要校验 Server 的真实性,Server 也需要校验 Client 的真实性。
  • 双向认证的原理:
    双向认证需要 Server 支持,Client 必须内置一套公钥证书 + 私钥。在 SSL/TLS 握手过程中,Server 端会向 Client 端请求证书,Client 端必须将内置的公钥证书发给 Server,Server 验证公钥证书的真实性。
    用于双向认证的公钥证书和私钥代表了 Client 端身份,所以其是隐秘的,一般都是用 .p12 或者 .bks 文件 + 密钥进行存放。
  • 代码层面如何做双向认证:
    双向校验就是自定义生成客户端证书,保存在服务端和客户端,当客户端发起请求时在服务端也校验客户端的证书合法性,如果不是可信任的客户端发送的请求,则拒绝响应。服务端根据自身使用语言和网络框架配置相应证书校验机制即可。
  1. 防止挂载抓包
  • 什么是挂载
  • Xposed黑科技:
    Xposed + JustTrustMe 可以破解绕过校验CA证书。那么这样CA证书的校验就形同虚设了,对App的危险性也很大。
  • App多开运行在多个环境上:
    多开App的原理类似,都是以新进程运行被多开的App,并hook各类系统函数,使被多开的App认为自己是一个正常的App在运行。
    一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App。
  • VirtualApp黑科技:
    它破坏了Android 系统本身的隔离措施,可以进行免root hook和其他黑科技操作,你可以用这个做很多在原来APP里做不到事情,于此同时Virtual App的安全威胁也不言而喻。
  • 判断是否具有Xposed环境:
  • 第一种方式:获取当前设备所有运行的APP,根据安装包名对应用进行检测判断是否有Xposed环境。
  • 第二种方式:通过自造异常来检测堆栈信息,判断异常堆栈中是否包含Xposed等字符串。
  • 第三种方式:通过ClassLoader检查是否已经加载了XposedBridge类和XposedHelpers类来检测。
  • 第四种方式:获取DEX加载列表,判断其中是否包含XposedBridge.jar等字符串。
  • 第五种方式:检测Xposed相关文件,通过读取/proc/self/maps文件,查找Xposed相关jar或者so文件来检测。
  • 判断是否是双开环境:
  • 第一种方式:通过检测app私有目录,多开后的应用路径会包含多开软件的包名。还有一种思路遍历应用列表如果出现同样的包名,则被认为双开了。
  • 第二种方式:如果同一uid下有两个进程对应的包名,在"/data/data"下有两个私有目录,则该应用被多开了。
  • 判断了具有xposed或者多开环境怎么处理App:
  • 目前使用VirtualApp挂载,或者Xposed黑科技去hook,前期可以先用埋点统计。测试学而思App发现挂载在VA上是推出App。
  1. 数据加解密
  • 第一步:获取请求的数据。主要是获取请求url和requestBody,这一块需要对数据一块处理。
  • 第二步:对请求数据进行加密。采用RC4加密数据。
  • 第三步:根据不同的请求方式构造新的request。使用 key 和 result 生成新的 RequestBody 发起网络请求。
  1. 数据加解密证书锁定
  • 证书锁定是Google官方比较推荐的一种校验方式:
    原理是在客户端中预先设置好证书信息,握手时与服务端返回的证书进行比较,以确保证书的真实性和有效性。
  • 如何实现证书锁定:
  • 一种通过network_security_config.xml配置
    第一种方式:配置文件
    域名:api.zuoyebangxxx
    38JpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhK90=
    9k1a0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM90K=
  • 另一种通过代码设置;
    val pinners = CertificatePinner.Builder()
    .add(“api.zuoyebangxxx”, “sha256//89KpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRh00L=”)
    .add(“api.zuoyebangxxx”, “sha256//a8za0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1o=09”)
    .build()
    val builder = OkHttpClient.Builder()
    builder.apply {
        certificatePinner(pinners)
    }
  1. Sign签名:为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证。
  • 需要对请求参数进行签名验证,签名方式如下:key1=value1&key2=value2&key3=value3&secret=yc 。对这个字符串进行md5一下
  • 服务端对sign校验:
    合法正确签名sign才可以获取数据。这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。再也无法伪造合法的请求

五,CPU 和 ABI 的关系
CPU 架构是 CPU 厂商定义的 CPU 规范,目前主流的 CPU 架构分为两大类:

  • 复杂指令集(CISC): 例如 Intel 和 AMD 的 X86 架构;
  • 精简指令集(RISC): 例如 IBM 的 PowerPC 架构、ARM 的 ARM 架构。

Android 支持 的 ABI

ABI描述
armeabi第 5 代、第 6 代的ARM 处理器,基本退出历史舞台
armeabiv-v7a第 7 代及以上的 ARM 处理器,正在逐步退出历史舞台
arm64-v8a第 8 代、64 位 ARM 处理器,目前是主流
x86 / x86_64一般是模拟器

Android构建64位apk:ndk.abiFilters 配置

android {
    ...
    defaultConfig {
        ...
        ndk {
            abiFilters "armeabi-v7a","arm64-v8a"
        }
    }
}

六,你不一定知道的Android小知识

  • 子线程未必不能更新UI
  • Android的UI访问是没有加锁的,多线程访问时并不安全。所以规定只能在UI线程中访问UI。
    负责检查线程的就是 ViewRootImpl 的 checkThread() 方法:
  • 然而 ViewRootImpl 的创建在 onResume() 回调之后。那么在 onResume() 之前,子线程里也是可以更新 UI 的 。
  • 即使是 ViewRootImpl 创建后,只要不调用 checkThread(),子线程里更新也并不会报错。
  • 代码 new 的 View 没有id
  • Android布局文件中通过 @+id 的方式,可以在 R文件 中生成对应的一个Int值,用于在运行时保证资源唯一性,但动态在代码中 new 的 View 没有 id 。
  • 如果你有需求使用它的id,可以调用 View 的 generateViewId() 方法来生成 id(API17+) ,而非用随机数产生或手写一个具体的值。
  • View 的 getContext 返回的未必是Activity
  • Activity中setContentView时一定是Activity;
  • 通过 new View、View.inflate、LayoutInflater.inflate 这几种方式添加View,我们传参时传的是什么context, View中的就是什么Context.
  • 在5.0系统版本以下的手机,且 Activity 是继承自 AppCompatActivity 的,那么View的getConext方法,返回的就不是Activity而是TintContextWrapper。
  • boolean 类型占几个字节
  • java中 boolean 表示的实际信息是一位:1表示true,0表示false。但是,Java规范 数据类型文档 没有精确定义内存中布尔变量的实际大小。
  • 其大小与虚拟机相关,可以肯定的是,不会是 1 个 bit 。
  • Java 虚拟机的建议如下:
  • 1.boolean 类型被编译成 int 类型来使用,占 4 个 byte 。
  • 2.boolean 数组被编译成 byte 数组类型,每个 boolean 数组成员占 1 个 byte
  • 硬件加速不是哪里都能开关
  • 硬件加速在Window级只能开不能关;View级只能关不能开。
  • Application 和 Activity 控制,在 AndroidManifest 文件中 Application 或 Activity 节点添加
    android:hardwareAccelerated=“true”
  • Window控制
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
  • View控制
    view.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
  • 查询是否开启硬件加速
    View.isHardwareAccelerated()
    Canvas.isHardwareAccelerated()
  • 用 getVisibility() 判断用户是否能看见并不好
  • getVisibility()只判断它自身是否是显示状态。但是如果它的父级不可见呢?
  • 用 isShown() 方法更合适些,会先判断当前 View 的flag, 然后循环拿到父View,判断是不是可见。只要有一个是不可见的,就返回false。

七,Fragment

  • getActivity()空指针:
    这种情况一般发生在在异步任务里调用getActivity(),而Fragment已经onDetach(),此时就会有空指针,解决方案是在Fragment里使用
    一个全局变量mActivity,在onAttach()方法里赋值,这样可能会引起内存泄漏,但是异步任务没有停止的情况下本身就已经可能内存泄漏,相比直接crash,这种方式
    显得更妥当一些。
  • Fragment视图重叠:
    在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)

八,SurfaceView

  • SurfaceView中包含一个Surface对象,而Surface是可以在后台线程中绘制的,SurfaceView的性质决定了其比较适合一些场景:需要界面迅速更新、对帧率要求较高的情况
  • 在绘制线程中必须先合法的获取 Surface 才能开始绘制内容, SurfaceHolder.Callback.surfaceCreated() 和 SurfaceHolder.Callback.surfaceDestroyed() 之间的状态为合法的,另外在Surface类型为SURFACE_TYPE_PUSH_BUFFERS时候是不合法的。 额外的绘制线程会消耗系统的资源,在使用SurfaceView的时候要注意这点
  • 只要继承SurfaceView类并实现SurfaceHolder.Callback接口就可以实现一个自定义的SurfaceView了,SurfaceHolder.Callback在底层的Surface状态发生变化的时候通知View,SurfaceHolder.Callback具有如下的接口:
  • surfaceCreated(SurfaceHolder holder):当Surface第一次创建后会立即调用该函数。程序可以在该函数中做些和绘制界面相关的初始化工作,一般情况下都是在另外的线程来绘制界面,所以不要在这个函数中绘Surface。
  • surfaceChanged(SurfaceHolder holder, int format, int width,int height):当Surface的状态(大小和格式)发生变化的时候会调用该函数,在surfaceCreated调用后该函数至少会被调用一次。
  • surfaceDestroyed(SurfaceHolder holder):当Surface被摧毁前会调用该函数,该函数被调用后就不能继续使用Surface了,一般在该函数中来清理使用的资源。
  • 通过SurfaceView的getHolder()函数可以获取SurfaceHolder对象,Surface 就在SurfaceHolder对象内。虽然Surface保存了当前窗口的像素数据,但是在使用过程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或则Canvas lockCanvas(Rect dirty)函数来获取Canvas对象,通过在Canvas上绘制内容来修改Surface中的数据。如果Surface不可编辑或则尚未创建调用该函数会返回null,在unlockCanvas() 和 lockCanvas()中Surface的内容是不缓存的,所以需要完全重绘Surface的内容,为了提高效率只重绘变化的部分则可以调用lockCanvas(Rect dirty)函数来指定一个dirty区域,这样该区域外的内容会缓存起来。
  • 在调用lockCanvas函数获取Canvas后,SurfaceView会获取Surface的一个同步锁直到调用unlockCanvasAndPost(Canvas canvas)函数才释放该锁,这里的同步机制保证在Surface绘制过程中不会被改变(被摧毁、修改)。
  • 当在Canvas中绘制完成后,调用函数unlockCanvasAndPost(Canvas canvas)来通知系统Surface已经绘制完成,这样系统会把绘制完的内容显示出来。为了充分利用不同平台的资源,发挥平台的最优效果可以通SurfaceHolder的setType函数来设置绘制的类型,目前接收如下的参数:
  • SURFACE_TYPE_NORMAL:用RAM缓存原生数据的普通Surface
  • SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬件加速的Surface
  • SURFACE_TYPE_GPU:适用于GPU加速的Surface
  • SURFACE_TYPE_PUSH_BUFFERS:表明该Surface不包含原生数据,Surface用到的数据由其他对象提供,在Camera图像预览中就使用该类型的Surface,有Camera负责提供给预览Surface数据,这样图像预览会比较流畅。如果设置这种类型则就不能调用lockCanvas来获取Canvas对象了。

九 kotlin-compiler-embeddable 下载慢的问题

  1. 打开网址 https://mvnrepository/
  2. 搜索kotlin-compiler-embeddable
  3. 进入kotlin-compiler-embeddable
  4. 点入所需jar包,file后面下载所需jar包版本
  5. 配置下载jar文件到.gradle文件中
  • 文件路径:/Users/“用户名”/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable/1.3.70/
  1. 重启AS

十 java判断点在三角形内

  • java中,在给定三角形的三个顶点和一个待判断的点的情况下,我们可以使用一些数学知识来判断该点是否在三角形内。
  • 首先,我们可以使用向量叉积的方法来进行判断。给定三角形顶点A、B、C,以及待判断的点P。我们可以计算向量AP、BP和CP与向量AB、BC和CA的叉积,分别表示为cross1、cross2和cross3。如果这三个叉积均为正数或者均为负数,那么点P位于三角形ABC内部。如果至少有一个叉积为0,则表示该点在三角形ABC上。
  • 其次,我们还可以通过计算三个子三角形的面积之和与原三角形面积进行比较来判断。同样给定三角形顶点A、B、C,以及待判断的点P。我们可以计算出子三角形ABP、BCP和CAP分别的面积,并将它们加起来得到sum_areas。然后再计算出原三角形ABC的面积area。如果sum_areas等于area,则表示该点位于三角形ABC内部;如果sum_areas小于area,则表示该点在三角形ABC外部。
  • 最后,在Java中,我们可以编写一个方法来实现上述算法,并返回一个布尔值表示待判断的点是否在给定的三角形内。
public static boolean isPointInTriangle(Point A, Point B, Point C, Point P) {
    // 计算向量叉积
    double cross1 = (P.x - A.x) * (B.y - A.y) - (P.y - A.y) * (B.x - A.x);
    double cross2 = (P.x - B.x) * (C.y - B.y) - (P.y - B.y) * (C.x - B.x);
    double cross3 = (P.x - C.x) * (A.y - C.y) - (P.y - C.y) * (A.x - C.x);

    // 判断是否在三角形内部
    if((cross1 > 0 && cross2 > 0 && cross3 > 0) || 
       (cross1 < 0 && cross2 < 0 && cross3 < 0)) {
        return true;
    }

    // 判断是否在三角形上
    if(cross1 == 0 || cross2 == 0 || cross3 == 0){
        return true;
    }
    
    return false;
}

十一 RK 开发板源码编译

  1. RK官网
  • https://www.rock-chips/
  1. RK开发工具,AndroidSDK网址
  • https://www.t-firefly/doc/download/page/id/3.html#other_144
  • https://wiki.t-firefly/zh_CN/Firefly-RK3399/compile_android8.1_firmware.html
  1. RK开发板AndroidSDK和AndroidStudio的AndroidSDK区别
  • RK开发板AndroidSDK是指为RK系列开发板定制的Android开发工具包,包含了开发板的驱动程序、库文件、示例代码等,以便开发者可以在RK开发板上进行Android应用的开发和调试。
  • 而AndroidStudio的AndroidSDK是指Google官方提供的Android开发工具包,包含了Android平台的API、工具和库文件等,用于开发和调试Android应用。
  • 两者的区别主要在于适用对象和功能。RK开发板AndroidSDK主要针对RK系列开发板,提供了特定的驱动和示例代码,方便开发者在RK开发板上进行Android应用的开发和调试;而AndroidStudio的>* AndroidSDK适用于所有的Android设备,提供了全面的Android开发工具和资源,可用于开发和调试任何Android设备上的应用。
  • 总结来说,RK开发板AndroidSDK是为RK系列开发板定制的Android开发工具包,而AndroidStudio的AndroidSDK是Google官方提供的通用Android开发工具包。

十二 so库冲突解决

  1. 出现错误的问题日志
2 files found with path 'lib/arm64-v8a/libc++_shared.so' from inputs:
 - D:\AndroidProject\juaismapp\opencvsdk\build\intermediates\library_jni\debug\jni\arm64-v8a\libc++_shared.so
 - D:\AndroidCache\.gradle\caches\transforms-3\74c11438533af106ed521e9aba5a6698\transformed\jetified-mmkv-1.0.22\jni\arm64-v8a\libc++_shared.so
If you are using jniLibs and CMake IMPORTED targets, see
  1. 解决:
    这是以为opencv和mmkv里面都有 ibc++_shared.so 库,造成冲突,我们只需在build.gradle中的android节点下将这些重复的so文件依次声明优先使用第一个即可
android {
   ........

    packagingOptions {
        pickFirst 'lib/arm64-v8a/libc++_shared.so'
        pickFirst 'lib/armeabi-v7a/libc++_shared.so'
        pickFirst 'lib/x86/libc++_shared.so'
        pickFirst 'lib/x86_64/libc++_shared.so'
    }
}

十三 Handle内存泄漏原因?

  • Handler内存泄漏的主要原因是由于持有Activity或Fragment的隐式引用导致的。当Handler对象持有Activity或Fragment的引用时,如果Activity或Fragment被销毁而Handler对象仍然存在,就会导致内存泄漏。

这种情况通常发生在以下几种情况下:

  • 在Activity或Fragment中创建Handler对象,并在Handler中处理消息时引用Activity或Fragment。
  • 在子线程中创建Handler对象,并在Handler中处理消息时引用Activity或Fragment。

为了避免Handler内存泄漏,可以采取以下几种方法:

  • 使用静态内部类来持有Handler对象,避免持有外部Activity或Fragment的引用。
  • 使用弱引用(WeakReference)来持有Activity或Fragment的引用,避免强引用导致的内存泄漏。
  • 在Activity或Fragment销毁时,及时移除Handler对象的消息队列中的消息和回调,避免Handler继续持有Activity或Fragment的引用。

通过以上方法可以有效地避免Handler内存泄漏问题,确保应用程序的内存使用正常,提高应用程序的性能和稳定性。

十四 为何主线程可以new handle,如果想要在子线程中new Handle需要做什么

  • 主线程可以直接在其中创建和使用Handler,因为Android应用程序的主线程(也称为UI线程)默认已经创建了Looper对象,从而可以支持Handler的消息处理机制。
  • 如果想要在子线程中创建Handler,需要先创建一个Looper对象,并调用Looper的prepare()和loop()方法来初始化和启动消息循环。这样就可以在子线程中使用Handler来发送和处理消息了。
  • 以下是在子线程中创建Handler的基本步骤:
  • 在子线程中创建Looper对象,并调用Looper的prepare()方法初始化Looper。
  • 调用Looper的loop()方法启动消息循环,使得该子线程可以处理消息。
  • 在子线程中创建Handler对象,并将其与该子线程的Looper关联。
  • 可以通过Handler发送消息到该子线程的消息队列中,并在Handler中处理这些消息。
  • 需要注意的是,在子线程中创建Handler时,需要确保在Looper的loop()方法被调用之前创建Handler对象,否则会导致RuntimeException。另外,在使用完Handler后,应该及时移除消息队列中的消息和回调,避免内存泄漏问题。
  • 总的来说,要在子线程中创建Handler,需要先创建并启动Looper对象,然后创建Handler并与该Looper关联,这样就可以在子线程中使用Handler来进行消息处理了。

十五 Android 子线程中维护的looper,消息队列无消息的时候处理方案是什么?有什么用?

  • 当Android子线程中维护的Looper的消息队列中没有消息时,可以通过调用Looper的loop()方法来等待消息的到来。Looper会在消息队列为空时进入阻塞状态,直到有新的消息到达队列中才会继续处理消息。
  • 在消息队列无消息时等待的处理方案有以下几种:
  • 阻塞等待:调用Looper的loop()方法会使当前线程进入阻塞状态,直到有新的消息到达才会继续处理消息。这种方式适用于需要实时处理消息的场景,例如网络请求的响应、定时任务等。
  • 轮询等待:可以在消息队列为空时,定时轮询消息队列是否有新的消息到达。这种方式可以避免线程长时间处于阻塞状态,提高响应速度。
  • 空闲处理:可以在消息队列为空时执行一些空闲处理操作,例如释放资源、执行定时任务等。这样可以充分利用子线程的空闲时间,提高系统的整体效率。
  • 消息队列无消息时等待的处理方案可以确保子线程能够及时响应新的消息,并且可以充分利用子线程的资源。通过合理选择适合的等待处理方案,可以提高系统的性能和稳定性,确保消息的及时处理。

十六 多个Handle往MessageQueue中添加数据,内部是如何保证线程安全的?

  • 在Android中,多个Handler往MessageQueue中添加消息时,内部通过以下方式保证线程安全:
  • 同步机制:MessageQueue内部使用了同步机制来保证多个Handler同时往消息队列中添加消息时的线程安全性。具体来说,MessageQueue内部使用了锁(Lock)或者同步块(Synchronized Block)来确保在多线程环境下对消息队列的操作是线程安全的。
  • 原子操作:对于消息队列的添加操作,通常会使用原子操作来确保操作的完整性。这可以避免多个线程同时对消息队列进行操作时出现数据不一致的情况。
  • 消息处理顺序:消息队列会按照消息的时间戳(when字段)来排序,保证消息的处理顺序是按照消息的发送顺序来进行的。这样可以避免消息处理的混乱和错乱。
  • Handler与Looper的关联:每个Handler都与一个特定的Looper对象相关联,而Looper又与一个特定的线程相关联。这种关联关系确保了每个Handler在特定的线程上操作消息队列,避免了多线程操作的竞争和混乱。
  • 总的来说,Android中通过同步机制、原子操作、消息处理顺序和Handler与Looper的关联等方式来保证多个Handler往消息队列中添加数据时的线程安全性。这些机制确保了消息队列的正确处理和消息的有序执行,提高了系统的稳定性和可靠性。

十六 AndroidStudio下载gradle巨慢解决办法

  • 项目级gradle-wrapper.properties里面修改为腾讯的下载地址
    https://mirrors.cloud.tencent
distributionBase=GRADLE_USER_HOME
#distributionUrl=https\://services.gradle/distributions/gradle-8.0-bin.zip
distributionUrl=https://mirrors.cloud.tencent/gradle/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

  • 同时为了防止下载一些lib也慢,把build.gradle里面的repositories也都修改成国内的
    maven { url ‘https://maven.aliyun/repository/google’}
    maven { url ‘https://maven.aliyun/repository/jcenter’}
    maven { url ‘http://maven.aliyun/nexus/content/groups/public’}
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
        maven { url 'https://maven.aliyun/repository/google'}
        maven { url 'https://maven.aliyun/repository/jcenter'}
        maven { url 'http://maven.aliyun/nexus/content/groups/public'}
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://maven.aliyun/repository/google'}
        maven { url 'https://maven.aliyun/repository/jcenter'}
        maven { url 'http://maven.aliyun/nexus/content/groups/public'}
    }
}

十七 JVM运行时数据区域

  • 堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器。
  • Heap(堆):
  • 对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域;开启逃逸分析后,某些未逃逸的对象可以通过标量替换的方式在栈中分配。
  • 堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区。
  • 方法区:
  • 对于JVM的方法区也可以称之为永久区,它储存的是已经被java虚拟机加载的类信息、常量、静态变量;Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace);
  • 当应用中的 Java 类过多时,比如 Spring 等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出。
  • 虚拟机栈:
  • 虚拟机栈是线程私有的,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(局部变量表、操作数栈 、动态链接 、返回地址);在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 局部变量表:局部变量表是一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量。底层是变量槽(variable slot)
  • 操作数栈:是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。
  • 动态链接:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。
  • 返回地址(returnAddress):类型(指向了一条字节码指令的地址)
  • JIT即时编译器(Just In Time Compiler),简称 JIT 编译器:
  • 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,比如锁粗化等。
  • 本地方法栈:
  • 本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
  • PC程序计数器:
  • PC,指的是存放下一条指令的位置的一个指针。它是一块较小的内存空间,且是线程私有的。由于线程的切换,CPU在执行的过程中,需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的PC。

十八 垃圾回收算法

  • 垃圾回收算法:复制算法、标记清除、标记整理、分代收集
  • 复制算法:(young)
  • 将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收;
  • 优点:实现简单,内存效率高,不易产生碎片
  • 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低
  • 标记清除:(cms)
  • 标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象
  • 缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,需要预留空间给分配阶段的浮动垃圾
  • 标记整理:(old)
  • 标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题
  • 分代收集:
  • 根据各个年代的特点选择合适的垃圾收集算法。
  • 新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
  • 老年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
  • Safepoint 当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为 JVM 是安全的(safe),整个堆的状态是稳定的。如果在 GC 前,有线程迟迟进入不了 safepoint,那么整个 JVM 都在等待这个阻塞的线程,造成了整体 GC 的时间变长。
  • MinorGC、MajorGC、FullGC
  • MinorGC 在年轻代空间不足的时候发生,
  • MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC。
  • FullGC 1、当老年代无法再分配内存的时候;2、元空间不足的时候;3、显示调用 System.gc 的时候。另外,像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 的时候也会发生 FullGC。
  • 对象优先在 Eden 区分配
  • 大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
  • 大对象直接进入老年代
  • 大对象是指需要连续内存空间的对象,比如很长的字符串以及数组。老年代直接分配的目的是避免在 Eden 区和 Survivor 区之间出现大量内存复制。
  • 长期存活的对象进入老年代
  • 虚拟机给每个对象定义了年龄计数器,对象在 Eden 区出生之后,如果经过一次 Minor GC 之后,将进入 Survivor 区,同时对象年龄变为 1,增加到一定阈值时则进入老年代(阈值默认为 15)
  • 动态对象年龄判定
  • 为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到阈值才能进入老年代。如果在 Survivor 区中相同年龄的所有对象的空间总和大于 Survivor 区空间的一半,则年龄大于或等于该年龄的对象直接进入老年代。
  • 空间分配担保
  • 在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间总和,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立则进行 Full GC。

十九 垃圾收集器

  • JDK3:Serial Parnew 关注效率
  • Serial:
    Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。适合用于客户端垃圾收集器。
  • Parnew:
    ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
  • JDK5:parallel Scavenge+(Serial old/parallel old)关注吞吐量
  • parallel Scavenge:(关注吞吐量)
    Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
  • Serial old:
  • Serial收集器的⽼年代版本,它同样是⼀个单线程收集器,使用标记-整理算法。主要有两个用途:
  • 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
  • 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
  • parallel old:
  • Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。
  • JDK8-CMS:(关注最短垃圾回收停顿时间)
  • CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
  • 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
  • 并发标记:进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
  • 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
  • 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
  • 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
  • 优点:并发收集、低停顿
  • 缺点:对CPU资源敏感;⽆法处理浮动垃圾;使⽤“标记清除”算法,会导致⼤量空间碎⽚产⽣。
  • JDK9-G1:(精准控制停顿时间,避免垃圾碎片)
  • 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是:
  • 【1】基于标记-整理算法,不产生内存碎片。
  • 【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
  • G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
  • 初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记;
  • 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢;
  • 最终标记:Stop The World,使用多条标记线程并发执行;
  • 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行;
  • JDK11-ZGC:(在不关注容量的情况获取最小停顿时间5TB/10ms)
  • 着色笔技术:加快标记过程
  • 读屏障:解决GC和应用之间并发导致的STW问题
  • 支持 TB 级堆内存(最大 4T, JDK13 最大16TB)
  • 最大 GC 停顿 10ms
  • 对吞吐量影响最大,不超过 15%

十九 Android Handle屏障消息,同步消息,异步消息的区别?

  • 在Android中,Handler中的屏障消息、同步消息和异步消息有以下区别:
  • 屏障消息(Barrier Message):
  • 屏障消息是一种特殊类型的消息,用于在消息队列中设置一个屏障,确保在该屏障之前的所有消息都被处理完毕后,才能继续处理屏障后面的消息。
  • 屏障消息可以用于控制消息的执行顺序,确保一组消息按照特定顺序执行。
  • 屏障消息可以通过Handler的sendMessageAtFrontOfQueue()方法发送。
  • 同步消息(Synchronous Message):
  • 同步消息是指通过Handler的sendMessage()方法发送的消息,会被立即处理,即在sendMessage()方法返回之前就会执行。
  • 同步消息会在当前线程中执行,不会放入消息队列中等待处理,因此可以立即执行。
  • 异步消息(Asynchronous Message):
  • 异步消息是指通过Handler的sendMessageDelayed()方法发送的消息,会在一定延迟之后才被处理。
  • 异步消息会被放入消息队列中,等待处理线程从消息队列中取出并执行。
    异步消息可以延迟执行,可以用于实现定时任务或延迟操作。
  • 总结:
  • 屏障消息用于控制消息的执行顺序,确保一组消息按照特定顺序执行。
  • 同步消息会立即执行,不会放入消息队列中等待处理。
  • 异步消息会被延迟执行,会放入消息队列中等待处理。
  • 通过合理使用这些消息类型,可以更好地控制消息的处理顺序和时机,从而提高应用程序的稳定性和性能。

二十 什么是GCRoot

  • 在Android开发中,GCRoot(Garbage Collection Root)是指一组特殊的对象,它们被认为是垃圾回收器的根节点,即垃圾回收器从这些根节点开始遍历,确定哪些对象是活动的(被引用)哪些是垃圾(不再被引用)。
  • 在Android中,GCRoot主要包括以下几种类型:
  • 虚拟机栈中的引用:即当前线程中正在执行的方法中使用的引用。
  • 本地方法栈中的引用:即通过JNI(Java Native Interface)调用的本地方法中使用的引用。
  • 方法区中的静态引用:即类的静态变量引用的对象。
  • 方法区中的常量引用:即类中定义的常量引用的对象。
  • Java堆中的引用:即通过软引用、弱引用、虚引用等方式引用的对象。
  • GCRoot对象保证了这些对象在内存中的可达性,即它们可以被访问到,不会被垃圾回收器回收。垃圾回收器会从这些GCRoot对象开始遍历,逐步确定哪些对象是活动的,哪些是垃圾对象,最终将垃圾对象进行回收释放内存。
  • 理解GCRoot对于Android开发者来说很重要,因为正确地管理内存和避免内存泄漏是保证应用性能和稳定性的关键。通过了解GCRoot的概念和类型,开发者可以更好地设计和优化应用程序,避免不必要的内存占用和泄漏问题。

二十一 Handle之idleHandle作用

  • 在Android开发中,Handler是用于在Android应用程序中处理消息和任务的类。Handler类中的handleMessage(Message msg)方法用于处理消息。当Handler接收到消息时,会调用handleMessage(Message msg)方法来处理消息。
  • 在Handler类中,有一个概念叫做"Idle Handler"(空闲处理程序),它是Handler类中的一个内部类。Idle Handler可以在消息队列空闲时执行某些任务,通常用于延迟执行一些任务,以免影响主线程的性能。如以下实用示例:
  • Activity启动优化,onCreate,onStrart,onResume这些耗时较短但非必要的代码可以放到idleHandle中执行,减少启动时间
  • 想要一个view绘制完成之后添加其它依赖于这个view的view,当然view.post也能实现,区别是前者会在消息队列空闲时执行
  • 发生一个返回true的idleHandle,在里面让某个view不断闪烁,这样然让用户等待时间诱导用户点击这个view,这是一个很酷的操作
  • 当消息队列中没有消息需要处理时,Idle Handler会被调用执行。可以通过Handler的postIdle(Runnable r)方法将任务添加到Idle Handler中,以便在消息队列空闲时执行。这样可以避免在消息队列繁忙时执行耗时任务,从而提高应用程序的响应性能。
  • 总之,Idle Handler是Handler类中的一个内部类,用于在消息队列空闲时执行任务。通过将任务添加到Idle Handler中,可以延迟执行一些任务,以避免影响主线程的性能。

二十二 Android 匿名内部类为什么持有外部类的对象?

  • 在Java中,匿名内部类持有外部类的引用是因为匿名内部类可能需要访问外部类的成员变量和方法。由于匿名内部类没有自己的名称,无法定义自己的成员变量和方法,因此需要依赖外部类的成员变量和方法来完成一些操作。
  • 当在匿名内部类中引用外部类的成员变量或方法时,编译器会自动为匿名内部类生成一个对外部类对象的引用。这样匿名内部类就可以通过这个引用来访问外部类的成员变量和方法。
  • 虽然这种设计可以方便匿名内部类访问外部类的成员,但也可能导致一些问题,比如内存泄漏。如果匿名内部类的生命周期比外部类长,就会导致外部类对象无法被垃圾回收,从而造成内存泄漏。
  • 因此,在使用匿名内部类时,需要注意潜在的内存泄漏问题,并采取相应的措施来避免这些问题,比如在匿名内部类中使用外部类成员变量时,将外部类对象的引用赋值给一个局部变量。

二十三 Android 静态内部类为什么不会持有外部类的对象

  • 在Java中,静态内部类不会持有外部类的对象引用,因为静态内部类是静态的,它与外部类的对象实例是独立的。静态内部类在编译时就会被加载并初始化,与外部类的对象实例无关。
  • 由于静态内部类不持有外部类的对象引用,因此在静态内部类中无法直接访问外部类的非静态成员变量和方法。如果需要访问外部类的非静态成员,可以通过创建外部类的对象来实现。
  • 因为静态内部类不持有外部类对象的引用,所以不会出现因为持有外部类对象而导致的内存泄漏问题。静态内部类的独立性使得它更加灵活和安全,不会对外部类对象的生命周期造成影响。
  • 总之,静态内部类不会持有外部类的对象引用,这使得静态内部类与外部类的关系更加独立,避免了一些潜在的问题。

本文标签: 第二篇知识android