WarriorYu

山中莫道无供给,明月清风不用钱


  • 首页

  • 标签

  • 分类

  • 归档

Android技术小Tip

发表于 2020-08-05 | 分类于 Android
  1. LinearLayout等空间在布局的根节点设置margin无效,padding有效。

  2. CheckBox

    • 自定义样式使用android:button="@drawable/selector_edit_activity_checkbox",但是paddingLeft设置的是图片与文字的间距,不是左间距,可以通过marginLeft设置左间距。
    • 如果只需要更改CheckBox的颜色,可以使用android:buttonTint="@color/black",但是有个版本的限制条件:Attribute buttonTint is only used in API level 21
    • CheckBox 如果在代码中设置了 cbxCheck.setOnClickListener(new View.OnClickListener(){} 会导致没有点击后的涟漪效果
  3. Imageview 的tint属性:可以将图片的背景色改为想要的颜色

    • 在xml文件中 android:tint="@color/white"

    • 在java代码中 imagview.setColorFilter(color);

    • 扩展

      How to apply the color. The standard mode is{@link PorterDuff.Mode#SRC_ATOP}

      setColorFilter(int color, PorterDuff.Mode mode)

  4. 前后台接口里的字段类型一定一致,否则会出现不可预知的bug:

    • 如果后台字段是String类型,前台是long类型,如果后台传了null,可能会导致奔溃,如果后台是long类型,不会出现null,即使没有这个字段,前台也会有默认值,减少崩溃率。不要每个字段判空,这是最low的,会导致项目里全是if else.
  5. 如果想下载的sdk不能通过代理下载,注意在gradle.properties文件里,把相关代码删除或者注释。

    报错信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve com.mob:MobTools:+.

    Could not resolve com.mob:MobTools:+.
    Required by:
    project :app
    > Could not resolve com.mob:MobTools:+.
    > Failed to list versions for com.mob:MobTools.
    > Unable to load Maven meta-data from http://jcenter.bintray.com/com/mob/MobTools/maven-metadata.xml.
    > Could not get resource 'http://jcenter.bintray.com/com/mob/MobTools/maven-metadata.xml'.
    > Could not GET 'http://jcenter.bintray.com/com/mob/MobTools/maven-metadata.xml'.
    > Connect to 127.0.0.1:1080 [/127.0.0.1] failed: Connection refused (Connection refused)
    > Connection refused (Connection refused)

    解决方式:关闭代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    android.enableAapt2=false
    #如果需要代理把这两行放开
    #systemProp.http.proxyHost=127.0.0.1
    #systemProp.http.proxyPort=1080
    org.gradle.jvmargs=-Xmx2048m -XX\:MaxPermSize\=512m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
    org.gradle.daemon=true
    org.gradle.configureondemand=true
    org.gradle.parallel=true
    android.useDeprecatedNdk=true
  6. RecyclerView等列表控件,如果获取position的item时要注意有header和没有header的情况,比如:

    1
    2
    3
    4
    以BaseRecyclerViewAdapterHelper 这个库举例:

    //因为有addHeaderView,所以position的计算一定不能少了减去getHeaderLayoutCount()
    int position = mViewHolder.get(i).getLayoutPosition() - getHeaderLayoutCount();
  7. Recyclerview:adapter.notifyDataSetChanged()类似的刷新方法,会引起已缓存的ViewHolder相应的onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)方法的调用,因为复用,并不会把整个列表都刷新一遍

  8. 代码压缩、混淆相关 :参见:https://developer.android.com/studio/build/shrink-code?hl=zh-cn

  9. TextView的自动适应文字大小属性:app:autoSizeTextType=”uniform”兼容到API14,

    如果android:autoSizeTextType=”uniform”不能兼容Android 8.0以下的手机。和android:singleLine不兼容,会显示“…”,可以使用android:maxLines=“1” 。width和height要写死,否则在Recyclerview中滑动的时候,可能会导致缩放不符合预期。

  10. 抽象类A实现接口B,A可以实现B中的部分方法,也可以全部实现。

  11. 音频详情问题:1.为什么PendingIntent的数据没有传过来。是序列化的问题,改为传递基本类型 2.为什么在initViews()调用finish方法,还会走到fillData();

  12. 沉浸式状态栏,根布局”android:fitsSystemWindows”的影响要注意。

音视频开发分享(星陨)

发表于 2020-08-05 | 分类于 杂谈

星陨:音视频开发分享

  1. 音视频开发主要是做什么

    • 音视频采集
      • Camera相关
    • 编码与解码
      • FFmpeg相关
        • 视频编解码、合成、压缩
    • 渲染
      • 特效相关,美颜滤镜、算法等
  2. 如何点亮音视频技能树

    IOS掌握好Fundation就足够入门了

    Android:

    • 学习Android上的音视频API
    • 学习OpenGL渲染与FFmpeg
    • 学习开源项目源码

    OpenGL学习了之后,该怎么做?

    • OpenGL在音视频中的作用
      • FFmpeg->OpenGL->Screen
    • OpenGL算是另一门学科了
      • 学习更多渲染和图形学方面的内容,比如光照、阴影
      • 学习渲染引擎方面的开发,可以学学游戏的

    掌握了技能点之后如何进阶?

    • 系统设计与框架搭建能力
    • 站在业务角度发展与进步
  3. Android 音视频入门项目

    • Android-gpuimage:偏向渲染,掌握OpenGL渲染链和Shader的写法
    • AudioVideoRecordingSample:偏向录制,实现Android上的音频和视频录制
    • google/grafika:综合运用,OpenGL和视频录制等多个样例
  4. 转行与内推

    • 音视频学习与转行是两码事

      XX有风险,入市需谨慎

    • 项目经历

      没有工作经历,要有开源项目,要体现出能力

    • 市场风向

      组建技术团队之时,对市场要有敏感度

  5. 音视频的上层业务基本是对接SDK,专业的是底层技术

  6. 搜商:搜索能力

移动Web(1)

发表于 2020-08-05 | 分类于 大前端
          • 1. 概念
          • 2. 适配要求
          • 3. 适配设置
          • 4 . viewport的功能
          • 5. 非主流的适配方案:
          • 6. 不建议在移动端使用jquery
          • 7. box-sizing
          • 8. 点击高亮
          • 9. 防止出现图片下间隙
          • 10.移动端滑动事件:
          • 11.click在移动端有300ms的延时:
          • 12.区域滚动效果:
          • 13.媒体查询
          • 14.reset.css 和normalize.css区别
          • 15.响应式布局
          • 16.自定义字体图标
          • 17.boostrap实现导航条
          • 18.模板引擎
          • 补充:
          • 注意事项:
          • MarkDown快捷键:
1. 概念
  • 流式布局:就是百分比布局,非固定像素,内容向两侧填充,理解成流动的布局,称为流式布局
  • 视觉窗口:viewport,是移动端特有。这是一个虚拟的区域,承载网页的。
  • 承载关系:浏览器—->viewport—->网页
2. 适配要求
  • 网页宽度必须和浏览器保持一致
  • 默认显示的缩放比例和PC端保持(缩放比例1.0)
  • 不允许用户自行缩放网页
  • 满足这些要求达到了适配,国际上通用的适配方案,标准的移动端适配方案。
3. 适配设置
  • 如果任何设置都没有,默认走的就是viewport的默认设置
  • 去设置新的viewport设置,达到适配要求, 设置视口的标签 在head里面并且应该紧接着编码设置
4 . viewport的功能
  • width 可以设置宽度 (device-width 当前设备的宽度)
  • height 可以设置高度
  • initial-scale 可以设置默认的缩放比例
  • user-scalable 可以设置是否允许用户自行缩放
  • maximum-scale 可以设置最大缩放比例
  • minimum-scale 可以设置最小缩放比例
    • 在 content=”” 使用以上参数
      1. width=device-width 宽度一致比例是1.0
      2. initial-scale=1.0 宽度一致比例是1.0
      3. user-scalable=no 不允许用户自行缩放 (yes,no 1,0)
  • 标准适配方案
1
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=0">

快捷方式:meta:vp + tab

5. 非主流的适配方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.页面的真实尺寸会比在设备的上尺寸要大几倍
2.假设设备是iphone4 -> 320px -> 网页尺寸 640px
3.缩放操作,有2倍的 有3倍 和屏幕像素比有关系
4.什么是屏幕像素(物理像素,像素点) px(页面的尺寸单位)
5.物理像素 是设备显示屏的最小可视颗粒的大小 以前的手机(直板手机)
6.现在有 高清显示屏 视网膜屏 retina屏
7.显示的效果就提高了更细腻,但是在显示同等质量的图片的时候(模糊效果)

8.在屏幕像素比(一个px宽的屏幕能放几个物理像素)高的设备 图片(非矢量)显示会模糊
9.提高网页的清晰度 根据屏幕的像素比 来缩放网页
10.但是这样的适配方案成本非常高
11.一般的企业开发当中使用的还是标准化设置

在高清显示屏当中:图片可能会失真(模糊)
6. 不建议在移动端使用jquery
1
2
3
4
5
1.可以使用jquery,但是不建议
2.jquery 做了很多桌面浏览器的兼容问题 特别是IE,但是移动端没有IE浏览器
3.主流的浏览器:谷歌 火狐(2016年停止了维护和更新) safari浏览器 百度 360 qq ...
4.特点:内核基本上都是 webkit 或者 blink 兼容 -webkit-
5.使用H5的api 或者说使用一个 叫做: zepto.js 的库(基于高版本浏览器开发)
7. box-sizing
1
2
3
4
5
6
7
/*防止内容溢出  不出现滚动条  提供用户体验*/
box-sizing: border-box;

1. 移动端以流式布局为主
2. 百分比布局
3. 非固定像素布局
4. 无法准确计算容器的尺寸
8. 点击高亮
1
2
/*轻击 轻触*/
-webkit-tap-highlight-color: red;
9. 防止出现图片下间隙

本质是有文字导致的,比如:agfy, g比a在格式上下沉,这样的文字和图片在一起,就会出现图片下间隙。

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
body{
/*font-size: 0px;*/
}
或
img{
display: block;
}
或
img{
vertical-align: middle;
}
</style>
10.移动端滑动事件:

​ touchstart touchmove touchend

11.click在移动端有300ms的延时:

​ 避免300ms的方案:

1.tap事件

​ 2.fastclick插件使用

12.区域滚动效果:

​ 插件 isScroll

13.媒体查询

​ 1、使用媒体查询能针对不同屏幕区间设置不同的布局和样式

​ 2、怎么使用媒体查询:关于媒体查询 @media

​ 3、语法: @media screen and (max-width: 768px) and (min-width: 320px){属性样式}

​ eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@media screen and (max-width: 768px) {
/*1. 在超小屏设备的时候 768px以下 当前容器的宽度100% 背景蓝色*/
.container{
width: 100%;
background: blue;
}
}
@media screen and (min-width: 768px) and (max-width: 992px){
/*2. 在小屏设备的时候 768px-992px 当前容器的宽度750px 背景绿色*/
.container{
width: 750px;
background: green;
}
}
@media screen and (min-width: 992px) and (max-width: 1200px){
/*3. 在中屏设备的时候 992px-1200px 当前容器的宽度970px 背景红色*/
.container{
width: 970px;
background: red;
}
}
@media screen and (min-width: 1200px){
/*4. 在大屏设备的时候 1200px以上 当前容器的宽度1170px 背景黄色*/
.container{
width: 1170px;
background: yellow;
}
}
14.reset.css 和normalize.css区别
1
2
共同点:都是重置样式库,增强浏览器的表现一致性
不同点:都是增强浏览器的表现一致性,但是normalize不会重置已经一致的元素
15.响应式布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!--响应式布局容器-->
<div class="container">
<!--栅格系统:其实就是行和列的布局,网格状布局-->
<!--行:row-->
<!-- .container容器默认有15px的左右内间距 .row 填充父容器的15px的左右内间距 margin-left,margin-right -15px拉伸 -->
<div class="row">
<!--列:col-*-* *不确定(参数) -->
<!--
col:列样式
第一个*:屏幕的大小
大屏设备 lg 大屏设备以上生效包含本身
中屏设备 md 中屏设备以上生效包含本身
小屏设备 sm 小屏设备以上生效包含本身
超小屏设备 xs 超小屏设备以上生效包含本身
<!--
响应式容器:
1. 在超小屏设备的时候 768px以下 当前容器的宽度100%
2. 在小屏设备的时候 768px-992px 当前容器的宽度750px
3. 在中屏设备的时候 992px-1200px 当前容器的宽度970px
4. 在大屏设备的时候 1200px以上 当前容器的宽度1170px
-->

第二个*:每一行的分等份,默认分成12等份 ,数字代表的是占多少份
-->
<div class="col-xs-4"></div>
<div class="col-xs-4"></div>
<div class="col-xs-4"></div>
</div>
</div>

<!--
大屏设备 显示
中屏设备 隐藏
小屏设备 显示
超小屏设备 隐藏
visible-lg 大屏显示其他隐藏
visible-md
visible-sm
visible-xs
3.2版本以后 建议使用hidden
hidden-lg
hidden-md
hidden-sm
hidden-xs
-->
16.自定义字体图标
1
2
3
4
5
6
7
8
9
10
11
/*自定义字体图标*/
/*1.通过@font-face定义自己的字体*/
@font-face {
/*2.申明自己的字体名称*/
font-family: 'wjs';
/*3.引入字体文件(约束某一段字符代码什么图案)*/
src: url(../fonts/MiFie-Web-Font.svg) format('svg'),
url(../fonts/MiFie-Web-Font.eot) format('embedded-opentype'),
url(../fonts/MiFie-Web-Font.ttf) format('truetype'),
url(../fonts/MiFie-Web-Font.woff) format('woff');
}
17.boostrap实现导航条
1
2
3
4
5
6
7
8
9
10
<!--切换按钮-->
<!--
类名:collapsed 样式
属性:
data-toggle="collapse" 申明是什么组件=折叠组件
data-target="#bs-example-navbar-collapse-1" 控制的目标元素=选择器
其他:
aria-expanded="false" aria-* 代表提供给屏幕阅读器使用的(盲人阅读器)
class="sr-only" screen read only 代表提供给屏幕阅读器使用的(盲人阅读器)
-->
18.模板引擎
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--使用模版引擎-->
<script type="text/template" id="pointTemplate">
<% for(var i = 0 ; i < list.length ; i ++){ %>
<li data-target="#carousel-example-generic" data-slide-to="<%=i%>" class=" <%=i==0?'active':''%>"></li>
<% } %>
</script>
<script type="text/template" id="imageTemplate">
<% for(var i = 0 ; i < list.length ; i ++){ %>
<div class="item <%=i==0?'active':''%>">
<% if(isMobile){ %>
<a href="#" class="m_imgBox"><img src="<%=list[i].mUrl%>" alt=""></a>
<% }else{ %>
<a href="#" class="pc_imgBox" style="background-image:
url(<%=list[i].pcUrl%>)"></a>
<% } %>
</div>
<% } %>
</script>
补充:
  1. a.offsetWidth; 获取控件的宽度

  2. document.querySelector(‘.class_name); 获取标签

  3. box.querySelectorAll(‘li’); 获取li标签数组

  4. imageBox.style.transition = ‘all 0.2s’;

    imageBox.style.webkitTransition = ‘all 0.2s’; css3属性要做兼容

  5. 浮动的元素因为脱离标准文档流没有高度,当设置touch事件的时候会无法响应事件,所以要清除浮动。

  6. dom.addEventListener(‘touchmove’,function(e){

    ​ var moveX = e.touches[0].clientX;

    })

  7. 实现两栏,左侧一栏设定:width:100px,float:left ; 右侧一栏设置overflow:hidden。可实现右侧占满屏幕,从而实现两栏。

  8. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    1、css代码
    /* +,~选择器 + 紧邻的下一个兄弟元素 ~ 后面所有的兄弟元素*/
    .wjs_topBar > .container > .row > div {
    height: 40px;
    line-height: 40px;
    text-align: center;
    }

    .wjs_topBar > .container > .row > div ~ div {
    border-left: 1px solid #ccc;
    }

    2、html代码
    <header class="wjs_topBar hidden-sm hidden-xs">
    <div class="container">
    <div class="row">
    <div class="col-md-2"></div>
    <div class="col-md-5"></div>
    <div class="col-md-2"></div>
    <div class="col-md-3"></div>
    </div>
    </div>
    </header>
注意事项:
  1. js方法调用一定不能忘记加小括号“()”,因为不会报错,eg: e.prevertDefault();
  2. tranform:translateX(1px); 能提高层级

集合概述

发表于 2020-08-05 | 分类于 Java基础

1: Collection集合总结(掌握)

​ Collection

​ |–List 有序,可重复

​ |–ArrayList

​ 底层数据结构是数组,查询快,增删慢。

​ 线程不安全,效率高

​ |–Vector

​ 底层数据结构是数组,查询快,增删慢。

​ 线程安全,效率低

​ |–LinkedList

​ 底层数据结构是链表,查询慢,增删快。

​ 线程不安全,效率高

​ |–Set 无序,唯一

​ |–HashSet

​ 底层数据结构是哈希表。

​ 如何保证元素唯一性的呢?

​ 依赖两个方法:hashCode()和equals()

​ 开发中自动生成这两个方法即可

​ |–LinkedHashSet

​ 底层数据结构是链表和哈希表

​ 由链表保证元素有序

​ 由哈希表保证元素唯一

​ |–TreeSet

​ 底层数据结构是红黑树。

​ 如何保证元素排序的呢?

​ 自然排序

​ 比较器排序

​ 如何保证元素唯一性的呢?

​ 根据比较的返回值是否是0来决定

2:Set集合(理解)

​ (1)Set集合的特点

​ 无序,唯一

​ (2)HashSet集合(掌握)

​ A:底层数据结构是哈希表(是一个元素为链表的数组)

​ B:哈希表底层依赖两个方法:hashCode()和equals()

​ 执行顺序:

​ 首先比较哈希值是否相同

​ 相同:继续执行equals()方法

​ 返回true:元素重复了,不添加

​ 返回false:直接把元素添加到集合

​ 不同:就直接把元素添加到集合

​ C:如何保证元素唯一性的呢?

​ 由hashCode()和equals()保证的

​ D:开发的时候,代码非常的简单,自动生成即可。

​ E:HashSet存储字符串并遍历

​ F:HashSet存储自定义对象并遍历(对象的成员变量值相同即为同一个元素)

​ (3)TreeSet集合

​ A:底层数据结构是红黑树(是一个自平衡的二叉树)

​ B:保证元素的排序方式

​ a:自然排序(元素具备比较性)

​ 让元素所属的类实现Comparable接口

​ b:比较器排序(集合具备比较性)

​ 让集合构造方法接收Comparator的实现类对象

​ C:把我们讲过的代码看一遍即可

​ (4)案例:

​ A:获取无重复的随机数

​ B:键盘录入学生按照总分从高到底输出

​

3: 针对Collection集合我们到底使用谁呢?(掌握)

​ 唯一吗?

​ 是:Set

​ 排序吗?

​ 是:TreeSet

​ 否:HashSet

​ 如果你知道是Set,但是不知道是哪个Set,就用HashSet。

​

​ 否:List

​ 要安全吗?

​ 是:Vector

​ 否:ArrayList或者LinkedList

​ 查询多:ArrayList

​ 增删多:LinkedList

​ 如果你知道是List,但是不知道是哪个List,就用ArrayList。

​

​ 如果你知道是Collection集合,但是不知道使用谁,就用ArrayList。

​

​ 如果你知道用集合,就用ArrayList。

​

4: 在集合中常见的数据结构(掌握)

​ ArrayXxx:底层数据结构是数组,查询快,增删慢

​ LinkedXxx:底层数据结构是链表,查询慢,增删快

​ HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()

​ TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序

​

HashMap-Java 1.7分析

发表于 2020-08-05 | 分类于 Java基础

HashMap Java 1.7 解析

  1. 定义:基于哈希表的Map接口实现类。

    继承了AbstractMap抽象类,实现了Map接口,实现了Cloneable接口,实现了Serializable接口。

  2. 不保证有序(如插入顺序)

  3. 初始容量是16,最大容量是2^30

  4. 允许 null 的键/值。key=null时,放在数组第0个位置,hash值为0。

    对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null。

  5. 非线程安全(非同步)。可通过 Collections.synchronizedMap(new HashMap())获得线程安全的HashMap.

  6. 在第一次put()时会初始化数组。

  7. 1
    2
    3
    4
    5
    6
    7
    /**
    * 函数源码分析2:indexFor(hash, table.length)
    * JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
    */
    static int indexFor(int h, int length) {
    return h & (length-1);
    // 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
  • 问题1: 为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

    为了解决 “哈希码与数组大小范围不匹配” 的问题,HashMap给出了解决方案:哈希码 与运算(&) (数组长度-1);

  • 问题2: 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?

    首先,数组长度是2的次幂,二进制表示就是首位为1,其他位都为0,如果直接和hash码做&运算的话,得到的数组索引结果都是偶数,相当于浪费了一般的数组空间。如果(数组长度-1)首位为1,低位为1,和hash码做&运算的结果有哈希码决定,可能为1或0,这样保证了计算后的数组存储位置的均匀性,降低了hash冲突。

    其次,h & (length-1)相当于h % length,即hash值对数组长度取模。之所以与(&)运算,而非求余(%),是为了提高运算效率。

  • 问题3:为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

    增大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突。

  • 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

    • final类型,保证了key的不可变性。
    • 内部已重写了hashCode() 、equals()方法。
  • HashMap 中的 key若 Object类型, 则需实现哪些方法?

    实现 hashCode() 、equals()。

    • hashCode():计算需存储数据的存储位置。实现不恰当会导致严重的hash冲突。
    • equals():比较存储位置上是否存在存储数据的键 Key,保证键 Key 在哈希表中的唯一性。存在,则更新value值即可。不存在,则直接插入数据。
  1. 插入前,先判断容量是否足够,若不足够,则进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标。
  2. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值。
  3. 扩容时,根据新容量(2倍容量)新建1个数组,即新table 。将旧数组上的数据(键值对)转移到新table中,从而完成扩容 。然后 重新设置阈值 threshold = (int)(newCapacity * loadFactor);
  4. 键值对的添加方式:单链表的头插法
    • 即 将该位置(数组上)原来的数据放在该位置的(链表)下1个节点中(next)、在该位置(数组上)放入需插入的数据-> 从而形成链表。
    • 在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况。
    • 此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 = 线程不安全 。
1
2
3
注意:由于 `JDK 1.8` 转移数据操作 = **按旧链表的正序遍历链表、在新链表的尾部依次插入** ,所以不会出现链表 **逆序、倒置** 的情况,故不容易出现环形链表的情况。

但 `JDK 1.8` 还是线程不安全,因为 无加同步锁保护
  1. 核心参数:
    • 数组(Table)的大小 =容量的大小(capacity)。默认容量是16。容量范围必须是2的次幂 & < 最大容量(2^30)。
    • 加载因子(loadFactor) = 0.75。
    • 扩容阈值(threshold) = 容量(capacity) * 加载因子(loadFactor)
    • HashMap 大小 = 数组长度 + 所有链表的长度。

参考:Java:这是一份全面 & 详细的HashMap 1.7源码分析指南

Java 并发核心知识体系

发表于 2020-08-05 | 分类于 Java基础

笔记

Java 并发核心知识体系精讲

一. 线程八大核心

第3章:实现线程的方式:

Introduction

thread1

thread1

thread1

第4章:线程启动

thread1

直接调用run()是在主线程或者当前子线程执行。而start()方法,先检查线程的状态,通过native方法start0()再启动新线程。

第5章:如何正确停止线程

1. 原理介绍:使用interrupt来通知,而不是强制。

2.

thread1

thread1

3.

thread1

4.

thread1

5.

thread1

thread1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Create : 2020/7/31
* Describe : 注意Thread.interrupted()方法的目标对象是"当前线程",而不管本方法来自于哪个对象
*/
public class RightWayInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {

}
}
});

threadOne.start();
threadOne.interrupt();
// 获取中断标志
System.out.println("isInterrupted:" + threadOne.isInterrupted());
// 获取中断标志并重置
// 因为interrupted()是静态方法,不管是threadOne还是Thread,
// 执行它的都是主函数main函数,main函数没有经过任何的中断,所以返回false。
System.out.println("isInterrupted:" + threadOne.interrupted());
// 获取中断标志并重置 原理同上
System.out.println("isInterrupted:" + Thread.interrupted());
// 获取中断标志
System.out.println("isInterrupted:" + threadOne.isInterrupted());
threadOne.join();
System.out.println("Main thread is over");

/**
* 打印结果:
*
* isInterrupted:true
* isInterrupted:false
* isInterrupted:false
* isInterrupted:true
*/
}
}

6.线程的 6个状态(生命周期)

  1. Java线程的6种状态及切换(透彻讲解):https://blog.csdn.net/pange1991/article/details/53860651

2.

thread1

3.

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  1. 阻塞(BLOCKED):表示线程阻塞于锁。一定是Synchronized修饰的代码块或者方法才有这个状态。

  2. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  3. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  4. 终止(TERMINATED):表示该线程已经执行完毕。

4.

thread1

5.

thread1

6.

thread1

根据上面第4点的图解答即可。

第7章 核心5:趣解Thread和Object类中线程相关方法

1.

thread1

2.

thread1

3.

thread1

4.

thread1

thread1

5.

thread1

6.

thread1

7.

thread1

8.

thread1

9.

thread1

第8章 核心6:一网打尽线程属性

thread1

thread1

第10章 核心8:追寻并发的崇高理想-线程安全

thread1

本文配套用于慕课网悟空老师的《Java并发核心知识精讲》课,本文所涵盖的知识导图的内容,在课程中有详细讲解。

课程地址:https://coding.imooc.com/class/362.html

以下思维导图会持续更新,所以请访问线上版本:

线程8大核心基础:

http://naotu.baidu.com/file/07f437ff6bc3fa7939e171b00f133e17?token=6744a1c6ca6860a0

Java内存模型——底层原理:

http://naotu.baidu.com/file/60a0bdcaca7c6b92fcc5f796fe6f6bc9?token=bcdbae34bb3b0533

死锁——从产生到消除:

http://naotu.baidu.com/file/ec7748c253f4fc9d88ac1cc1e47814f3?token=bb71b5895a747d67

二级大纲(复习用) 成体系的Java并发多线程核心+内存模型+死锁——从用法到原理,面试必考:

http://naotu.baidu.com/file/29942292cd032adfae23c09783676004?token=130df3d389cab703

并发工具类分类:

http://naotu.baidu.com/file/3902a010470d7c1cf76fe719be124797?token=bec25304cc071bf1

动画分类

发表于 2020-08-05 | 分类于 动画

动画总结:

  1. 补间动画:指的是TranslationAnimation,AlpahAnimation,ScaleAnimation,RotateAnimation; 缺点:并没有真正持久改变View的属性,就是说它内部没有一个去记录动画行为的机制;

  2. 帧动画:指的是一帧一帧播放的动画 实现:通过animation-list来实现,写法如下:

    1
    2
    3
    4
    5
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android" > 
    <item android:duration="200" android:drawable="@drawable/ic_launcher"/>
    <item android:duration="200" android:drawable="@drawable/ic_launcher"/>
    <item android:duration="200" android:drawable="@drawable/ic_launcher"/>
    </animation-list>
  1. 属性动画(为了解决补间动画的缺点)

    1. 属性动画内部实现:3.0之后view类增加新的用来记录动画行为的属性,如:

      • translationX,translationY;
      • scaleX,scaleY;
      • rotationX,rotationY,rotation;
      • alpha;

      具体实现的类:ObjectAnimator;

      问题是:ObjectAnimator只能是3.0之后才有,那么我们如果想让属性动画兼容低版本,那么一般 使用NineOldAnidroid.jar来实现属性动画

    2. NineOldAnidroid.jar:主要封装了属性动画和View相关的操作类,该类库是JackWharton来写的;

      用法:

      1
      2
      3
      4
      ViewPropertyAnimator.animate(text)
      .rotationBy(180)
      .setDuration(500)
      .start();
    3. 直接操作view的属性来更改view的形态:

      • view.setTranslationX();
      • view.setRotationX();
      • view.setAlpha();
      • view.setScaleX();

      //如果想在低版本直接操作view的属性,则用如下方法

      1
      ViewHelper.setScaleX(text, 0);
    4. 改变动画的运动轨迹:速度插值器

      • OvershootInterpolator:超过一点再回来
      • BounceInterpolator:像球落地一样的感觉
    5. 自定义动画逻辑:ValueAnimator(值动画)

      ValueAnimator: 它只是帮我们定义了动画的执行流程,但是没有帮我们实行具体的动画逻辑,我们需要监听动画的进度,然后在回调方法中进行自定义的动画逻辑; 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ValueAnimator animator = ValueAnimator.ofInt(100,1000); 
    //监听动画执行的进度
    animator.addUpdateListener(new AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
    int animatedValue = (Integer) animator.getAnimatedValue();
    //Log.e("tag", "animatorValue:"+animatedValue);
    //根据动画值的变化进行我们的动画逻辑 //
    LayoutParams params = text.getLayoutParams();
    //params.height = animatedValue;
    //text.setLayoutParams(params);
    text.setText(animatedValue+"");
    }
    });
    animator.setDuration(1500);
    animator.setStartDelay(1000);
    animator.start();

(二) 架构师晋级

发表于 2020-08-05 | 分类于 架构

2. 架构师晋级

一. 换肤核心技术

1. 首先分析View的创建流程

即分析AppCompatActivity的setContentView(R.layout.activity_main);

1
2
3
4
5
6
7
8
   public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//1.
setContentView(R.layout.activity_main);
}
}
1
2
3
4
5
6
7
8
9
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

@Override
public void setContentView(@LayoutRes int layoutResID) {
//2.
getDelegate().setContentView(layoutResID);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AppCompatDelegate {
//3.
public abstract void setContentView(@LayoutRes int resId);
}


class AppCompatDelegateImpl extends AppCompatDelegate
implements MenuBuilder.Callback, LayoutInflater.Factory2 {

//4. setContentViews是在这里实现的
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//我们看这个inflate方法,看是怎么填充view的
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public abstract class LayoutInflater {
//5.
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
//跟进这个方法
return inflate(resource, root, root != null);
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
//跟进这个方法
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// ... 省略部分代码
// Temp is the root view that was found in the xml
// 跟进这个方法
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
result = temp;
// ... 省略部分代码
return result;
}

@UnsupportedAppUsage
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
//6. 马上就能看到是怎么创建view的了
return createViewFromTag(parent, name, context, attrs, false);
}
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// ... 省略部分代码
try {
//7. 在这里创建view ->看下面的分析8
View view = tryCreateView(parent, name, context, attrs);
//9. 如果我们拦截view的创建后,还是为空,则走系统的方法
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
//控件名有[.]的则是自定义的控件,走这个方法
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
// ...
}

//8. 分析
public final View tryCreateView(@Nullable View parent, @NonNull String name,
// ....
View view;
// 如果我们提前做了工作,让mFactory2!=null,则调用了mFactory2.onCreateView
//我们在Actitivy中重写onCreateView 则拦截住了系统创建view的过程
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

return view;
}
}

综上分析,Activity在setContentView(R.layout.activity_main)的时候,通过mFactory2.onCreateView来创建view,我们在Activity中重写onCreateView拦截系统的方法,来创建自己可以实现换肤的控件,没有拦截的控件还是走系统的方法去创建。

既然找到了需要拦截的方法,那我们再看在哪里可以拦截这个方法。开始分析源码:

2. 源码分析,寻找突破点,即找一个可以换肤的节点。

1
2
3
4
5
6
7
8
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//1. 进入onCreate
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback, 
SupportParentable, DelegateProvider {

protected void onCreate(@Nullable Bundle savedInstanceState) {
AppCompatDelegate delegate = this.getDelegate();
//2. 进入installViewFactory
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && this.mThemeId != 0) {
if (VERSION.SDK_INT >= 23) {
this.onApplyThemeResource(this.getTheme(), this.mThemeId, false);
} else {
this.setTheme(this.mThemeId);
}
}

super.onCreate(savedInstanceState);
}
}
1
2
3
4
5
6
7
8
public abstract class AppCompatDelegate {
// If you are using your own Factory or Factory2 then you can omit this call, and // instead call createView(android.view.View, String, android.content.Context,
// android.util.AttributeSet) from your factory to return any compatible widgets.
// 源码注释:如果你想使用自定义的Factory or Factory2,要避免调用此方法,并且重写factory的 // createView方法

//3. installViewFactory是一个抽象方法,进入它的实现类继续查看
public abstract void installViewFactory();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2 {
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
// 在这里进行了判null的操作,如果我们自己设置了Factory,就阻止了下面的操作,就可以实现自定义
if (layoutInflater.getFactory() == null) {
//4. 此类实现了Factory2,因为setFactory2(layoutInflater, this)第二个参数是
//this,即把Factory2的实现类传进去,实现类实现了onCreateView方法。
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}

/**
* From {@link LayoutInflater.Factory2}.
* 5. 实现Factory2的onCreateView方法,在onCreateView方法又调用了createView方法
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return createView(parent, name, context, attrs);
}

/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}


@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
//7. 在这个createView方法里就是做了new AppCompatViewInflater()这个工作
// ... 省略部分代码
mAppCompatViewInflater = new AppCompatViewInflater();
// ... 省略部分代码

//8. 最后调用了刚才实例化的AppCompatViewInflater的createView方法
return mAppCompatViewInflater.createView(parent, name, context, attrs,
inheritContext,
IS_PRE_LOLLIPOP,
true,
VectorEnabledTintResources.shouldBeUsed()
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class AppCompatViewInflater {
//9. 第8步调用的这个方法。
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
// ... 省略部分代码
View view = null;

// We need to 'inject' our tint aware Views in place of the standard
// framework versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
view = createView(context, name, attrs);
}

//... 省略部分代码

return view;
}
}
1
2
3
4
5
6
7
8
public class Activity extends ContextThemeWrapper
//10. 通过查看Activity的源码,可以知道Activity实现了Factory2接口,所以我们可以直接在
// 自己新建的MainActivity中直接重写Factory2的onCreateView方法。
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {

通过以上分析我们得出结论:

  • Activity在onCreate方法里调用了super.onCreate(savedInstanceState) -> delegate.installViewFactory() -> LayoutInflaterCompat.setFactory2(layoutInflater, this) -> this是Factory2的实现类,实现类实现了onCreateView方法 ->mAppCompatViewInflater.createView()
  • mAppCompatViewInflater.createView() 在这里面通过name(即控件的标签名)初始化了对应的view。
  • 我们可以在初始化view的时候做工作,即把控件换成自定义控件,自定义控件加上可以动态换肤的工作。
  • 所以如果想实现换肤,可以实现mAppCompatViewInflater.createView()方法里的工作,又因为Factory2.onCreateView方法调用了这个方法,那么我们可以通过重写onCreateView()来实现目的,又因为Activity实现了Factory2,所以我们可以直接在自己的Activity中重写onCreateView()方法。
  • 通过查看AppCompatActivity.

3. 换肤实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends SkinActivity {

private String skinPath;

@Override
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
protected void onCreate(Bundle savedInstanceState) {
//1. 在这里调用了SkinActivity的onCreate
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (("皮肤包").equals(PreferencesUtils.getString(this, "当前皮肤"))) {
skinDynamic(skinPath, R.color.skin_item_color);
} else {
defaultSkin(R.color.colorPrimary);
}
}

// 换肤按钮(api限制:5.0版本)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void skinDynamic(View view) {
// 真实项目中:需要先判断当前皮肤,避免重复操作!
if (!("皮肤包").equals(PreferencesUtils.getString(this, "currentSkin"))) {
skinDynamic(skinPath, R.color.skin_item_color);
PreferencesUtils.putString(this, "currentSkin", "皮肤包");
}
}

// 默认按钮(api限制:5.0版本)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void skinDefault(View view) {
if (!("default").equals(PreferencesUtils.getString(this, "currentSkin"))) {
defaultSkin(R.color.colorPrimary);
PreferencesUtils.putString(this, "currentSkin", "default");
}
}

@Override
protected boolean openChangeSkin() {
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class SkinActivity extends AppCompatActivity {

private CustomAppCompatViewInflater viewInflater;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
//2. 在这里setFactory2(layoutInflater, this)方法,可以阻止
//AppCompatDelegateImpl.installViewFactory(),从而阻止了installViewFactory到
//AppCompatViewInflater.createView()的工作,即阻止了[源码分析]的4-9步。
//从而实现我们自定义控件的逻辑
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);
super.onCreate(savedInstanceState);
}

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet
attrs) {
if (openChangeSkin()) {
if (viewInflater == null) {
//3. 自定义CustomAppCompatViewInflater
viewInflater = new CustomAppCompatViewInflater(context);
}
viewInflater.setName(name);
viewInflater.setAttrs(attrs);
//4. 在这个方法里将布局中的控件,更换为自定义的换肤控件,从而实现换肤的功能。
return viewInflater.autoMatch();
}
return super.onCreateView(parent, name, context, attrs);
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
protected void defaultSkin(int themeColorId) {
this.skinDynamic(null, themeColorId);
}

/**
* 动态换肤(api限制:5.0版本)
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
protected void skinDynamic(String skinPath, int themeColorId) {
SkinManager.getInstance().loaderSkinResources(skinPath);

if (themeColorId != 0) {
int themeColor = SkinManager.getInstance().getColor(themeColorId);
StatusBarUtils.forStatusBar(this, themeColor);
NavigationUtils.forNavigation(this, themeColor);
ActionBarUtils.forActionBar(this, themeColor);
}

applyViews(getWindow().getDecorView());
}

/**
* 控件回调监听,匹配上则给控件执行换肤方法
*/
protected void applyViews(View view) {
if (view instanceof ViewsMatch) {
ViewsMatch viewsMatch = (ViewsMatch) view;
viewsMatch.skinnableView();
}

if (view instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) view;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
applyViews(parent.getChildAt(i));
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 自定义控件加载器(可以考虑该类不被继承)
*/
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {

private String name; // 控件名
private Context context; // 上下文
private AttributeSet attrs; // 某控件对应所有属性

public CustomAppCompatViewInflater(@NonNull Context context) {
this.context = context;
}

public void setName(String name) {
this.name = name;
}

public void setAttrs(AttributeSet attrs) {
this.attrs = attrs;
}

/**
* @return 自动匹配控件名,并初始化控件对象
*/
//4. 通过将布局文件里对应name的控件,替换为我们自己的换肤自定义控件,实现换肤操作。
//注意: 这里有的控件才会实现换肤,没有控件不会实现换肤
public View autoMatch() {
View view = null;
switch (name) {
case "LinearLayout":
// view = super.createTextView(context, attrs); // 源码写法
view = new SkinnableLinearLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "TextView":
view = new SkinnableTextView(context, attrs);
this.verifyNotNull(view, name);
break;
case "ImageView":
view = new SkinnableImageView(context, attrs);
this.verifyNotNull(view, name);
break;
case "Button":
view = new SkinnableButton(context, attrs);
this.verifyNotNull(view, name);
break;
}

return view;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* 继承TextView兼容包,9.0源码中也是如此
* 参考:AppCompatViewInflater.java
* 86行 + 138行 + 206行
*/
public class SkinnableTextView extends AppCompatTextView implements ViewsMatch {

private AttrsBean attrsBean;

public SkinnableTextView(Context context) {
this(context, null);
}

public SkinnableTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}

public SkinnableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

attrsBean = new AttrsBean();

// 根据自定义属性,匹配控件属性的类型集合,如:background + textColor
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.SkinnableTextView,
defStyleAttr, 0);
// 存储到临时JavaBean对象
attrsBean.saveViewResource(typedArray, R.styleable.SkinnableTextView);
// 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
typedArray.recycle();
}

//对Textview进行换肤
@Override
public void skinnableView() {
// 根据自定义属性,获取styleable中的background属性
int key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_background];
// 根据styleable获取控件某属性的resourceId
int backgroundResourceId = attrsBean.getViewResource(key);
if (backgroundResourceId > 0) {
// 是否默认皮肤
if (SkinManager.getInstance().isDefaultSkin()) {
// 兼容包转换
Drawable drawable = ContextCompat.getDrawable(getContext(), backgroundResourceId);
// 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
// setBackgroundDrawable本来过时了,但是兼容包重写了方法
setBackgroundDrawable(drawable);
} else {
// 获取皮肤包资源
Object skinResourceId = SkinManager.getInstance().getBackgroundOrSrc(backgroundResourceId);
// 兼容包转换
if (skinResourceId instanceof Integer) {
int color = (int) skinResourceId;
setBackgroundColor(color);
// setBackgroundResource(color); // 未做兼容测试
} else {
Drawable drawable = (Drawable) skinResourceId;
setBackgroundDrawable(drawable);
}
}
}

// 根据自定义属性,获取styleable中的textColor属性
key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_textColor];
int textColorResourceId = attrsBean.getViewResource(key);
if (textColorResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
setTextColor(color);
} else {
ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
setTextColor(color);
}
}

// 根据自定义属性,获取styleable中的字体 custom_typeface 属性
key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_custom_typeface];
int textTypefaceResourceId = attrsBean.getViewResource(key);
if (textTypefaceResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
setTypeface(Typeface.DEFAULT);
} else {
setTypeface(SkinManager.getInstance().getTypeface(textTypefaceResourceId));
}
}
}
}

二. 组件化框架设计

1. APT(Annotation Processing Tool)

  • 是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够正常运行,必须通过APT工具来进行处理。

  • 也可以这样理解,只有通过声明APT工具后,程序在编译期间自定义注解解释器才能执行。

  • 通俗理解:根据规则,帮我们生成代码、生成类文件。

2. 工具:

  1. square/javapoet:JavaPoet 是一个生成.java源文件的Java API.
  2. alibaba/ARouter: 帮助 Android App 进行组件化改造的路由框架 .

3. 组件化里路由的基本思想

  1. (创建注解)新建Java Library类型的arouter_annotation模块。在里面新建路由注解(ARouter)。

  2. (处理注解)新建Java Library类型的compiler模块。在里面新建继承AbstractProcessor的注解处理类,根据注解获取类的信息,以及注解上的参数等信息,来编写生成源代码的代码。最后在编译时生成源代码文件。

    • 使用的类库

    • 1
      2
      3
      4
      5
      6
      7
      8
      // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
      compileOnly'com.google.auto.service:auto-service:1.0-rc4'
      annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'

      // 帮助我们通过类调用的形式来生成Java代码
      implementation "com.squareup:javapoet:1.9.0"
      // 引入annotation,处理@ARouter注解
      implementation project(':arouter_annotation')
  3. (使用注解)@ARouter(path = “/app/MainActivity”)

三. 插件化框架设计

1. 原理:可安装运行的宿主APP ->可以通过插件化,运行调用下载到本地的没有安装的apk文件。

2. 占位(插桩)式插件化框架

  1. 基本思想:

    • 通过AssetManager获取sd卡中下载好的插件apk,可以获取Resources和DexClassLoader。
    • 新建公共库,定义标准(接口ActivityInterface),比如Activity、service的生命周期方法,以及获取宿主的Context等接口。
    • 作为插件的apk,里面的Activity实现标准接口ActivityInterface。
    • 宿主中新建一个占位(代理)的Activity(ProxyActivity),并且通过重写相关方法,替换为插件的Resources和DexClassLoader。
    • 当启动插件中的Activity时,先获取插件Activity的ActivityInfo,然后就可以通过反射获取到Activity对象,即ActivityInterface的实现类对象,通过这个对象就可以执行插件里面Activity的方法了。
    • 因为插件中使用的是宿主中的Activity,只需要在宿主中的清单文件中注册代理Activity就行,插件中其实不是真正的Activity,无需注册。
    • 总结为,在宿主中创建一个空的代理Activity,当调用插件中的Activity时,在代理Activity中的方法中调用插件中相同的方法。
  2. Activity、service使用插件的流程差不多,但是BroadcastReceivery有发送和接收两步,所以多了一步接收的操作。

  3. 静态注册的广播,是什么时候注册的?

    • 手机开机的时候,所有的app,再次进行安装一遍,系统会去解析AndroidManifest,解析静态广播,进行注册。
  4. 有关插件的问题

    • 在插件中,为什么不能使用 this
      答:因为插件是没有安装手机上安装的,是无法拥有组件环境的。

    • 为什么要有代理的Activity
      答:由于插件中的Activity,并不是一个能够运行的组件,所以需要代理的Activity去代替 插件中的Activity (例如:Activity任务进栈)。

    • 这种插件化,在写插件开发的时候有什么(要注意的事项)
      答:所有关于操作,组件环境的地方,都必须使用 宿主的环境。

  5. Apk解析原理系统源码分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    1.静态注册的广播,是什么时候注册的?
    手机开机的时候, 所有的app,再次进行安装一遍, 系统会去解析AndroidManifest,解析静态广播后,就会自动注册

    2.我们去分析 安装
    data/app 放置目录
    data/data/包名/ 应用所属目录
    data/dalvik-cache 虚拟机去加载执行指令

    3.该分析那个目录
    data/app 放置目录

    手机开机安装app的时候,安装 过后 马上就会 全盘扫描,data/app 放置目录

    解析出 app apk 文件 里面所有组件,包括权限,系统会去解析AndroidManifest

    Android 会在 安装过后,会马上扫描此目录data/app 放置目录 ---> 解析 apk 文件 里面的配置信息AndroidManifest.xml ,如果里面有静态配置的广播
    就会要去注册广播

    分析系统源码,是如何进行解析apk

    PackageManagerService

    【目标】:看系统是如何 去解析 apk 文件里面的 组件信息的
    系统是在安装的时候,才会去扫描,apk

    PackageManagerService

    Linux内核驱动 --- init进程 -- zygote进程 孵化 SystemServer --- PackageManagerService启动

    PackageManagerService启动
    pms 如何去 处理 data/app/目录 ,如何解析apk

    dataDir /data/目录

    mAppInstallDir = new File(dataDir, "app"); /data/app/目录
    mAppInstallDir:如何去解析apk文件的

    scanDirTracedLI 是要去扫描 /data/app/目录下的apk文件 ---> 解析AndroidManifest 里面的所有信息

    扫描 apk 文件,解析apk scanPackageLI --->

    parsePackage:解析 apk 文件里面的所有信息

    Package ---> apk 里面的 AndroidManifest配置信息 (所有的)

    拿到了Package,就能拿到 静态的广播信息

    最终的目标:
    <!-- 静态注册的广播 -->
    <receiver android:name=".StaticReceiver">

    <intent-filter>

    <action android:name="plugin.static_receiver" />

    </intent-filter>

    </receiver>

    总结为:PackageManagerService可以加载手机里的任何apk,从它的构造方法可以看出,安装的apk文件都在data/app目录里,当扫描这个目录里的某个apk文件时,,调用了PackageParser.parsePackage()方法,并且返回Package类,这个类有个字段属性receivers(ArrayList ),就是AndroidMannifest里的receiver,receivers里面的泛型Activity不是四大组件的Activity,通过这个Activity可以获取跟广播相关的一些信息。所以,我们可以通过反射,根据上面的源码,一步步获取AndroidManifest里有关广播的一些信息,然后在代码中注册广播即可。

3. Hook式插件化框架

  1. Hook 基础 – 从入门到熟练

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    1.替换(把系统里面的 替换成 动态代理)
    2.添加动态代理(做我们自己的业务逻辑)


    ---- Hook 系统源码 ----> TestActivity 不再AndroidManifest里面注册,也能启动
    会报错:TestActivity}; have you declared this activity in your AndroidManifest.xml? 没有在AndroidManifest里面注册

    原因:

    startActivity ---> TestActivity ----》 (Hook) (AMS)ActivityManagerService(检测,当前要启动的Activity是否注册了)

    Hook (Hook):
    1.把TestActivity 替换我们真实有效的Activity

    startActivity(TestActivity) ---> Activity --> Instrumentation.execStartActivity ---> ActivityManagerNative.getDefault()
    IActivityManager.startActivity ---> (Hook) AMS.startActivity(检测,当前要启动的Activity是否注册了)

    思想切入点:既然会得到IActivityManager,会设置IActivityManager,(寻找替换点(动态代理))

    动态代理:由于执行startActivity之前,我们需要先执行我们的代码(把TestActivity 替换成 已经注册的 Activity)


    2.ASM检查过后,要把这个ProxyActivity 换回来 --> TestActivity

    startActivity ---> TestActivity -- (Hook ProxyActivity)(AMS)检测,当前要启动的Activity是否注册了)ok ---》
    ActivityThread(即将加载启动Activity)----(要把这个ProxyActivity 换回来 --> TestActivity)

    Hook LAUNCH_ACTIVITY

    我们要在Handler。handleMessage 之前执行,就是为了(要把这个ProxyActivity 换回来 --> TestActivity),所有需要Hook


    1.hook ams检查 把TestActivity 换成 ProxyActivity
    2.hook 即将要加载Activity又把ProxyActivity 换回来了 TestActivity
  2. 类加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    学习类加载之前,我们去startActivity跳转 到 插件中的Activity看会发生什么错误(分析错误的过程中学习类加载)

    宿主 跳转 宿主的Actvity ok

    宿主 跳转 插件里面的Activity 报错
    分析错误原因,来学习Android类加载:
    Caused by: java.lang.ClassNotFoundException: Didn't find class "com.netease.plugin_package.PluginActivity" on path:
    DexPathList[[zip file "/data/app/com.netease.hookproject-1/base.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_
    dependencies_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_0_apk.apk", zip file "/data/app/com
    .netease.hookproject-1/split_lib_slice_1_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_2_apk.apk"
    , zip file "/data/app/com.netease.hookproject-1/split_lib_slice_3_apk.apk", zip file "/data/app/com.netease.hookproject-1/s
    plit_lib_slice_4_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_5_apk.apk", zip file "/data/app/com
    .netease.hookproject-1/split_lib_slice_6_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_7_apk.apk",
    zip file "/data/app/com.netease.hookproject-1/split_lib_slice_8_apk.apk", zip file "/data/app/com.netease.hookproject-1/spli
    t_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

    startActivity --> AMS ---> ActivityThread(把代理的Activity给换回来了) ---> 要去实例化Activity (报错)

    Activity --> Instrumentation ---> AMS检查 --->
    ActivityThread (即将加载)-(handleLaunchActivity 类加载Activity performLaunchActivity ---> newActivity(cl == PathClassLoader))


    分析Android中的ClassLoader:
    1.java中的ClassLoader 和 Android的ClassLoader 是不一样
    2.ClassLoader == PathClassLoader
    3.PathClassLoader == cl.loadClass(className).newInstance();

    PathClassLoader.loadClass ---》 BaseDexClassLoader --》ClassLoader.loadClass--findClass(空方法) 让覆盖的子类方法去完成 --》
    BaseDexClassLoader.findClass() ---》pathList.findClass

    BaseDexClassLoader.findClass() -- c 为什么为null,--》 DexPathList.findClass(className) ---》DexFile.loadClassBinaryName(系列步骤后 NDK)

    for遍历 dexElements == Element[] ,分析 Element 是什么 ,为什么Element.dexFile==null?

    Android虚拟机 dex文件的 dex == 对Dex表现形式的描述 Element --- dexFile拥有可执行

    为什么 Element ==null?
    答:就是因为类加载机制加载的是 ---》 宿主的 classes.dex--Elements, 【没有插件的Element】

    解决方案:把插件的dexElements 和 宿主中的 dexElements 融为一体 PathClassLoader 就能加载到 插件/宿主 都可以加载到了
    Hook式 插件化

    ------ Android ClassLoader介绍
    1.java中的ClassLoader 和 Android的ClassLoader 是不一样
    2.Android中的ClassLoader 分为两类:系统提供的ClassLoader ---》BootClassLoader,PathClassLoader,DexClassLoader
    自定义ClassLoader

    给系统预加载使用的 :BootClassLoader
    给程序/系统程序/应用程序 加载class的 PathClassLoader
    加载 apk zip apk文件 DexClassLoader

    1.内核启动 ...
    2.init第一个进程
    3.zygote进程
    // 启动是很早就要启动
    ---> zygoteInit --> BootClassLoader.getInstance(); handleSystemServerProcess PathClassLoaderFactory --》PathClassLoader
    4.zygote进程孵化 SystemServer
    5.SystemServer启动很多的服务 ---(AMS,PSM,...)
    // 不能在这里启动
  3. 宿主和插件融为一体,解决插件没有宿主环境等问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 第一步:找到宿主 dexElements 得到此对象     PathClassLoader代表是宿主

    // 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件

    // 第三步:创建出 新的 newDexElements [],类型必须是Element,必须是数组对象

    // 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements for遍历

    // 第五步:把新的 newDexElements,设置到宿主中去

    以上操作,就可以去加载插件里面的class



    需要去加载插件里面的 layout

    StringBlock ---》 string.xml color.xml anim.xml ...

    mStringBlocks[] == string.xml color.xml anim.xml

    只有mStringBlocks[] 初始化了,才能去加载 插件里面的资源
  4. LoadedApk

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    占位式 插件化 ---》(stander标准) 在插件中必须使用 宿主的环境 appActivity   宿主context  ---》 插件

    Hook式 (宿主 和 插件 element 进行融合) 在插件中可以随意使用this,既然式融合一起,插件可以使用到宿主的环境
    插件越多 内存中的 newDexElements 就会越大

    LoadedApk式 插件化框架的手写,我们控制 ClassLoader
    PathClassLoader ---> 宿主的class
    自定义ClassLoader --》 插件的class
    解决这个问题:插件越多 内存中的 newDexElements 就会越大

    ActivityThread源码的分析:

    startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》
    ActivityThread -- mH LAUNCH_ACTIVITY(自己处理LoaderApk中的ClassLoader)

    case LAUNCH_ACTIVITY: {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
    // 跳转的Activity纪录 --》
    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

    // 如果缓存mPackages中有LoadedApk 就直接返回,如果没有LoaaedApk就创建出LoadedApk ---》 宿主的LoadedApk.ClassLoader
    // 如果是加载插件,从mPackages取出 插件专用的LoadedApk.自定义ClassLoader
    r.packageInfo = getPackageInfoNoCheck(
    r.activityInfo.applicationInfo, r.compatInfo);

    // 真正的即将 实例化Activity 然后进行启动
    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    } break;


    1.public final LoadedApk getPackageInfoNoCheck == 宿主的

    2.缓存中的 final ArrayMap<String, WeakReference<LoadedApk>> mPackages 默认保存的是宿主的LoadedApk


    LoadedApk --- 宿主的 ----》 LoadedApk.ClassLoader ---> 宿主中的class
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); // LoadedApk里面的ClassLoader
    (Activity)cl.loadClass(className).newInstance(); 实例化的Activity --》 宿主的 LoadedApk里面的ClassLoader 去实例化的


    以上代码结论:宿主的LoadedApk.ClassLoader 去加载 宿主中的class,然后实例化的


    --- > 自定义一个LoadedApk 自定义一个ClassLoader 用于专门加载插件里面的class,然后实例化


    自定义一个 LoadedApk 然后保存 --》 mPackages

    LoadedApk --- 插件的 ----》 LoadedApk.ClassLoader ---> 插件中的class

    3.梳理流程:
    宿主: startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》ActivityThread
    mPackages.value取出 LoadedApk.ClassLoader ---> 实例化Activity (只能加载宿主的)

    插件(下一节课,要完成的功能):
    我们在取出之前,需要自定义一个 (插件专用的 LoadedApk 自定义ClassLoader) 添加到 --》 mPackages

    startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》ActivityThread
    mPackages.value取出 插件专用的LoadedApk.ClassLoader --> 实例化插件的Activity


    下节课的目标:绕过 PMS 的检查处理 -->

    流程:startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》ActivityThread --》

    --> 获取自定义的LoadedApk.ClassLoader --> 实例化 initializeJavaContextClassLoader(PMS检查要启动的包名是否安装)

    -->生命周期方法的处理 (才能真正启动加载到 插件包里面的Activity)

    PMS检测 插件包包名是否安装,如果没有安装就 会抛出异常:
    java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException:
    Unable to get package info for com.netease.plugin_package; is package not installed?

    pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
    UserHandle.myUserId());
    Hook 我们要在 getPackageInfo 执行之前 给Hook拦截住,控制pi不为null
    分析:pm.getPackageInfo 客户端进程 ----》 PMS进程-检测是否安装了
  5. 插件和宿主融为一体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 第一步:找到宿主 dexElements 得到此对象     PathClassLoader代表是宿主

    // 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件

    // 第三步:创建出 新的 newDexElements [],类型必须是Element,必须是数组对象

    // 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements for遍历

    // 第五步:把新的 newDexElements,设置到宿主中去

    以上操作,就可以去加载插件里面的class


    需要去加载插件里面的 layout

    StringBlock ---》 string.xml color.xml anim.xml ...

    mStringBlocks[] == string.xml color.xml anim.xml

    只有mStringBlocks[] 初始化了,才能去加载 插件里面的资源
  6. 总结

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    1.占位插件化 Activity  - ProxyActivity   --》插件里面,在插件开发中,必须时时刻刻记住是 是使用宿主的环境
    2.占位插件化 Service - ProxyService --》插件里面,在插件开发中,必须时时刻刻记住是 是使用宿主的环境
    3.占位插件化 动态广播 - ProxyReceiver --》插件里面,在插件开发中,必须时时刻刻记住是 是使用宿主的环境
    4.占位插件化 静态广播 -- 分析系统是如何解析APK文件的,源码的阅读,阅读源码的思路,PMS入手-(模仿系统是如何解析,我们就怎么解析)(难点)
    稳定,插件化开发很痛苦 -------》 开源中的框架 插件化框架 DL

    5.Hook从入门 到 熟练 --(1.替换,2.被替换的 动态代理/接口设置) --> Hook系统源码 -- (Hook1)AMS检测是否注册 --- (Hook2)换回来
    6.安卓的类加载 --》PathClassLoader(加载运行App中的class),DexClassLoader(apk zip), DexPathList Element,插件Element和宿主Element
    7.真正的融合--》就可以加载插件里面的class, 插件里面的Layout怎么去加载呢,AseetManager Resources (难点)
    Hook式插件化框架 --》 融为一体,Hook方式: 不用考虑宿主的环境,不稳定==兼容性, 360开源框架 Hook方式实现的

    8.LoadedApk插件化-- ActivityThread LAUNCH_ACTIVITY源码切入点,ClassLoader--》宿主 --> LoadedApk.ClassLoader --> 宿主class
    9.自定义LoadedApk.自定义ClassLoader ---> 插件的class, mPackages -- size=2 , 没法绕过PMS检测,检测插件包名是否安装了
    10.开关 切换 宿主 插件, 再次运行会报错,是因为 PMS检测, Hook PMS,绕过了pi==null的情况
    不稳定==兼容性
  7. Hook代码推理技巧

    1
    2
    3
    1.Hook反射处理,倒序写代码,只写一行代码,推理千万行代码

    2.兼容的 21 ~ 28 系统版本 -- Hook插件化框架 手写

(一) 架构师入门

发表于 2020-08-05 | 分类于 架构

1. 架构师入门

一. AOP面向切面架构设计-两种方式

  1. 定义:

    在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

  2. aspectj:

    • 编译时用Aspect专门的编译器,不再使用传统的javac
  3. aspectj相关:

    • 下载aspectj 地址 http://www.eclipse.org/aspectj/downloads.php

    • 下载aspectj的adt地址http://www.eclipse.org/ajdt/downloads/#43zips

  • build.gradle aspectJ 写法 http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/

二. 设计模式

  1. MVC: 如果Activity里有线程执行耗时操作没有执行完,但是退出Activity,这时Activity不会被回收,导致内存泄漏。
  2. MVP:Activity使用弱引用,当有耗时操作时,退出Activity时不会发生内存泄漏。
    • 第一种P层的实现:Google在Github开源的架构实例,M对应JavaBean,V对应Activity/Fragment, P处理业务逻辑等,所有的活都有P层干完,然后通知V更新UI。
    • 第二种P层的实现:P层只做转发,M对应Model层,P转发给M层处理业务逻辑等,然后M再返回给P层,P层通 知V层更新UI。
    • 第三种P层的实现:类似第二种,但是不需要Model层,让功能模块去工作(LIibrary:下载、请求、图片加载)。
  3. MVVM:
    1. 缺点
      • 虽然不用findViewById,但是view都放在一个Object数组里,Activity有很多,就有很多数组,这是第一个占用内存过多的地方。
      • 在ViewDataBinding类的static代码块里,在每个Activity都有OnAttachStateChangeListener对每个View做状态改变的监听,状态改变都会调用他的onViewAttachedToWindow方法,从该方法里能看到在ViewDataBinding类里new了Runnable,也就是创建了线程,可见Activity多的时候,会因为每个Activity都new了一个Runnable,线程过多导致内存占用过大。这是第二个占用内存的地方。
      • 在ViewDataBinding类的invalidateAll()方法,还有一些其他的地方,里面使用了Handler,因为Handler绑定了Looper,每个Activity都这样,占用内存过多。这是第三个占用内存的地方。
    2. M->V V->M的过程解析
      • ViewDataBinding类的static快里,执行了binding.mRebindRunnable.run();跟着方法走,可以看到executePendingBindings() ->executeBindingsInternal() ->executeBindings(),executeBindings这个方法在ActivityMainBindingImpl实现,里面可以跟踪看到M更新V,V更新M。

三. Android 系统源码

1. Handler: 为什么在子线程可以使用textView.setText() 不报错,使用Toast报错。

  • 首先这个问题不是绝对的,如果在textView.setText() 之前sleep几秒,同样会报错。

  • 因为setText()方法的过程中,会在ViewRootImpl类中执行一个checkThread方法,来检查当前线程和创建ViewRootImpl时的线程是不是同一个线程;而ViewRootImpl的创建是在onResume方法中初始化的,而我们开启的子线程是在onCreat方法中,这个时候程序没有去检测当前线程是不是主线程,所以没有抛异常。因为Toast也有setText方法,但是在这个方法之前还有一些其他操作,等执行到检查线程的方法时,ViewRootImpl已经初始化好了。如果我们在TextView.setText之前sleep一秒,就会报错,因为ViewRootImpl已经初始化好了,会检查线程。

2.Binder

​ 1. Binder可以实现进程与进程之间的通信。

(三)Glide的使用

发表于 2020-08-05 | 分类于 Android源码分析

Glide 记录

  1. Android SDK 要求

    Min Sdk Version - 使用 Glide 需要 min SDK 版本 API 14 (Ice Cream Sandwich) 或更高。

    Compile Sdk Version - Glide 必须使用 API 27 (Oreo MR1) 或更高版本的 SDK 来编译。

    Support Library Version - Glide 使用的支持库版本为 27。

    如果你需要使用不同的支持库版本,你需要在你的 build.gradle 文件里去从 Glide 的依赖中去除 "com.android.support"。例如,假如你想使用 v26 的支持库:

    1
    2
    3
    4
    5
    6
    dependencies {
    implementation ("com.github.bumptech.glide:glide:4.8.0") {
    exclude group: "com.android.support"
    }
    implementation "com.android.support:support-fragment:26.1.0"
    }
  2. 多数情况下,使用Glide加载图片非常简单,一行代码足矣:

    1
    2
    3
    Glide.with(fragment)
    .load(myUrl)
    .into(imageView);

    取消加载同样很简单:

    1
    Glide.with(fragment).clear(imageView);

    尽管及时取消不必要的加载是很好的实践,但这并不是必须的操作。实际上,当 Glide.with() 中传入的 Activity 或 Fragment 实例销毁时,Glide 会自动取消加载并回收资源。

  3. 在 ListView 和 RecyclerView 中的使用

    在 ListView 或 RecyclerView 中加载图片的代码和在单独的 View 中加载完全一样。Glide 已经自动处理了 View 的复用和请求的取消。对 url 进行 null 检验并不是必须的,如果 url 为 null,Glide 会清空 View 的内容,或者显示 placeholder Drawable 或 fallback Drawable 的内容。

  4. Glide允许用户指定三种不同类型的占位符,分别在三种不同场景使用:

    • placeholder
    • error
    • fallback
  5. 请求选项

    Glide中的大部分设置项都可以通过 RequestOptions 类和 apply() 方法来应用到程序中。

    使用 request options 可以实现(包括但不限于):

    • 占位符(Placeholders)
    • 转换(Transformations)
    • 缓存策略(Caching Strategies)
    • 组件特有的设置项,例如编码质量,或Bitmap的解码配置等。
123
WarriorYu

WarriorYu

Stay Hungry. Stay Foolish.

30 日志
13 分类
19 标签
GitHub E-Mail
© 2020 WarriorYu
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4