reoger的记录

--以后的你会感激现在那么努力的自己

0%

单元测试

在日常的开发过程中,为了保证代码的质量和减少代码出错的机率,我们常常会通过单元测试来验证我们的代码。
本篇博客就是用来记录单元测试的用法,以免遗忘。

junit 工具类

在android中编写单元测试的时候,用的最多的还是junit 4吧,在新版的android studio中,会默认添加依赖,如果是老项目,可能需要module在build.gradle中添加如下的依赖。

1
testImplementation 'junit:junit:4.12'

好了,环境准备完毕,然后就可以开始进行单元测试了。首先我们还是编写一个需要测试的类,简单起见就编写一个a+b的实现吧,代码如下:

1
2
3
4
5
6
7
8
9
public class Utils {

public int add(Integer a, Integer b) throws ParseException{
if (a == null || b== null){
throw new ParseException("param is null",0);
}
return a + b;
}
}

然后,就可以编写单元测试了,在window环境下,直接按ctrl+shift+T即可快速创建一个单元测试。如图:
图1

其中的注解我们稍后介绍,

1
2
3
4
5
6
7
8
9
10
11
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class UtilsTest {

@Test
public void testAdd() throws Exception {
Utils test = new Utils();
assertEquals(3,test.add(1,3));
}
}

例如我们可以编写一个这样的测试类,使用@Test标识这是一个测试,利用assertEquals来进行判断结果是否符合预期。
图2
很明显,这个测试并没有通过,因为1+3的结果明显不是3,但这是我们的预期的结果就错了,所以就需要更改预期为4,运行后发现测试通过。
下面简单介绍其他的注解和方法的使用。

注解

注解名 注解含义
@Test 表示此方法为测试方法
@Before 在每个测试方法前执行,可做初始化操作
@After 在每个测试方法后执行,可做释放资源操作
@Ignore 忽略的测试方法
@BeforeClass 在类中所有方法前运行。此注解修饰的方法必须是static void
@AfterClass 在类中最后运行。此注解修饰的方法必须是static void
@RunWith 指定该测试类使用某个运行器
@Parameters 指定测试类的测试数据集合
@Rule 重新制定测试类中方法的行为
@FixMethodOrder 指定测试类中方法的执行顺序

在测试流程中,上述注解的方法执行的顺序为:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
主语其他的非流程类的测试注解,@RunWith、@Parameters、@Rule、@FixMethodOrder,稍后会进一步介绍其用法。

使用@Parameters配置多组测试数据

在需要进行多组数据进行测试的时候,我们可能会这么写:

1
assertArrayEquals(new int[]{3,5,9},new int[]{test.add(1,2),test.add(2,3),test.add(4,5)});

是的,上述方式可以实现测试多组数据,我们甚至可以将他们分开一条一条的测试,以便于我们快速找到测试不通过的地方。但是有一种更优雅的方式,我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(Parameterized.class)
public class UtilsTest {
public Point point;

public UtilsTest(Point point) {
this.point = point;
}

@Test
public void testAdd() throws Exception {
Utils test = new Utils();
assertEquals(point.x+point.y,test.add(point.x,point.y));
}

@Parameterized.Parameters
public static Collection params(){
return Arrays.asList(new Point(1,2),new Point(3,4),new Point(5,6));
}
}

可以看到,我们在UtilsTest,添加了注解@RunWith(Parameterized.class),用于表示次测试类的参数是可变的,然后添加了一个有参的构造方法public UtilsTest(Point point),用于接受可变的参数,最后使用@Parameterized.Parameters 来构造参数列表,如此我们的测试类就可以实现一次运行多个测试样例了。结果如下所示:
图3

测试抛出异常

当我们需要测试传入异常参数时,是否会如预想的一样抛出异常,这个时候我们就可以使用到@test注解中的expected属性,例如,我们还是测试上面的a+b,测试代码如下:

1
2
3
4
5
@Test(expected = ParseException.class)
public void testAdd() throws Exception {
Utils test = new Utils();
assertEquals(5,test.add(null,5));
}

在测试类中,我们传入了nulladd方法中,我们预想这种肯定会出现异常,所以我们在Test注解中添加了expected来捕获ParseException异常,如果捕获到这个异常,此时通过,如果没有捕获到这个异常,则这个测试方法会主动抛出ParseException异常,并说名测试不通过。

Assert类的主要用法

方法名 方法描述
assertEquals 断言传入的预期值与实际值是相等的
assertNotEquals 断言传入的预期值与实际值是不相等的
assertArrayEquals 断言传入的预期数组与实际数组是相等的
assertNull 断言传入的对象是为空
assertNotNull 断言传入的对象是不为空
assertTrue 断言条件为真
assertFalse 断言条件为假
assertSame 断言两个对象引用同一个对象,相当于“==”
assertNotSame 断言两个对象引用不同的对象,相当于“!=”
assertThat 断言实际值是否满足指定的条件

匹配器

匹配器 说明 例子
is 断言参数等于后面给出的匹配表达式 assertThat(5, is (5));
not 断言参数不等于后面给出的匹配表达式 assertThat(5, not(6));
equalTo 断言参数相等 assertThat(30, equalTo(30));
equalToIgnoringCase 断言字符串相等忽略大小写 assertThat(“Ab”, equalToIgnoringCase(“ab”));
containsString 断言字符串包含某字符串 assertThat(“abc”, containsString(“bc”));
startsWith 断言字符串以某字符串开始 assertThat(“abc”, startsWith(“a”));
endsWith 断言字符串以某字符串结束 assertThat(“abc”, endsWith(“c”));
nullValue 断言参数的值为null assertThat(null, nullValue());
notNullValue 断言参数的值不为null assertThat(“abc”, notNullValue());
greaterThan 断言参数大于 assertThat(4, greaterThan(3));
lessThan 断言参数小于 assertThat(4, lessThan(6));
greaterThanOrEqualTo 断言参数大于等于 assertThat(4, greaterThanOrEqualTo(3));
lessThanOrEqualTo 断言参数小于等于 assertThat(4, lessThanOrEqualTo(6));
closeTo 断言浮点型数在某一范围内 assertThat(4.0, closeTo(2.6, 4.3));
allOf 断言符合所有条件,相当于&& assertThat(4,allOf(greaterThan(3), lessThan(6)));
anyOf 断言符合某一条件,相当于或 assertThat(4,anyOf(greaterThan(9), lessThan(6)));
hasKey 断言Map集合含有此键 assertThat(map, hasKey(“key”));
hasValue 断言Map集合含有此值 assertThat(map, hasValue(value));
hasItem 断言迭代对象含有此元素 assertThat(list, hasItem(element));

UI测试

上述的单元测试虽然功能很强大,但是局限性也很大,比如不能测试UI相关的,不能测试与Context相关的方法。我们可以利用Ui测试来进行这方面的测试。
关于ui测试,请参考这篇博客,实在写的太好了。
关于android studio中的Ui测试,可以参考官方文档
这里仅记录一下使用心得:

espresso必要的依赖

module中的build.gradle中添加下面三条依赖:

1
2
3
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

使用示例:
譬如在MainActivity中添加有一个如下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity  {


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

/**
* 需要测试的方法,简单的调起浏览器打开baidu首页
**/
public static void startActiviy(Context context){
Uri uri = Uri.parse("http://wwww.baidu.com");
Intent t = new Intent(Intent.ACTION_VIEW);
t.setData(uri);
t.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(t);
}
}

然后,我们还是可以选择ctrl+shift+T来创建测试类,选择android test目录即可。
然后我们可以编写一个类似于这样类来进行测试:

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
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();

assertEquals("com.example.cm.testpulgin", appContext.getPackageName());
}

@Test
public void testOpenUrl(){
MainActivity.startActiviy(InstrumentationRegistry.getTargetContext());
}

@Test
public void test1(){
assertEquals(1,1);
}
}

我们选择运行,选择目标手机,即可在手机上实现测试。这里关键的一点是利用InstrumentationRegistry.getTargetContext()获取到了Context对象。
我们可以利用这个Context对象来测试与手机密切相关的一些属性了,前面的单元测试只能测试代码在jvm上是否运行正常,而到了这里我们就可以测试代码是否在手机上运行正常了。
譬如我们要测试一些用户的操作的行为是否符合预期,我们可以写一个类似于这样的测试类:

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

@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

@Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);

@Test
public void mainActivityTest() {
ViewInteraction button = onView(
allOf(withId(R.id.but1),
childAtPosition(
childAtPosition(
withId(android.R.id.content),
0),
0),
isDisplayed()));
button.check(matches(isDisplayed()));

ViewInteraction appCompatButton = onView(
allOf(withId(R.id.but1), withText("测试hook-ams"),
childAtPosition(
childAtPosition(
withId(android.R.id.content),
0),
0),
isDisplayed()));
appCompatButton.perform(click());

}

private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {

return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}

@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
}

对其中的一些方法进行简单说明:

方法名 含义
click() 点击view
clearText() 清除文本内容
swipeLeft() 从右往左滑
swipeRight() 从左往右滑
swipeDown() 从上往下滑
swipeUp() 从下往上滑
click() 点击view
closeSoftKeyboard() 关闭软键盘
pressBack() 按下物理返回键
doubleClick() 双击
longClick() 长按
scrollTo() 滚动
replaceText() 替换文本
openLinkWithText() 打开指定超链

参考链接

WebView缓存原理分析和应用

发表于 2017-05-13 | 分类于 技术 |

一、背景

现在的App开发,或多或少都会用到Hybrid模式,到了WebView这边,经常会加载一些js文件(例如和WebView用来Native通信的bridge.js),而这些js文件不会经常发生变化,所以我们希望js在WebView里面加载一次之后,如果js没有发生变化,下次就不用再发起网络请求去加载,从而减少流量和资源的占用。那么有什么方式可以达到这个目的呢?先得从WebView的缓存原理入手。

二、WebView的缓存类型

WebView主要包括两类缓存,一类是浏览器自带的网页数据缓存,这是所有的浏览器都支持的、由HTTP协议定义的缓存;另一类是H5缓存,这是由web页面的开发者设置的,H5缓存主要包括了App Cache、DOM Storage、Local Storage、Web SQL Database 存储机制等,这里我们主要介绍App Cache来缓存js文件。

三、浏览器自带的网页数据缓存

1.工作原理

浏览器缓存机制是通过HTTP协议Header里的Cache-Control(或Expires)和Last-Modified(或 Etag)等字段来控制文件缓存的机制。关于这几个字段的作用和浏览器的缓存更新机制,大家可以看看这两篇文章(H5 缓存机制浅析 移动端 Web 加载性能优化Android:手把手教你构建 WebView 的缓存机制 & 资源预加载方案),里面有详细的介绍。下面从我实际应用的角度,介绍一下通常会在HTTP协议中遇到的Header。

这两个字段是接收响应时,浏览器决定文件是否需要被缓存;或者需要加载文件时,浏览器决定是否需要发出请求的字段。

  • Cache-Control:max-age=315360000,这表示缓存时长为315360000秒。如果315360000秒内需要再次请求这个文件,那么浏览器不会发出请求,直接使用本地的缓存的文件。这是HTTP/1.1标准中的字段。

  • Expires: Thu, 31 Dec 2037 23:55:55 GMT,这表示这个文件的过期时间是2037年12月31日晚上23点55分55秒,在这个时间之前浏览器都不会再次发出请求去获取这个文件。这是HTTP/1.0中的字段,如果客户端和服务器时间不同步会导致缓存出现问题,因此才有了上面的Cache-Control,当它们同时出现在HTTP Response的Header中时,Cache-Control优先级更高。

下面两个字段是发起请求时,服务器决定文件是否需要更新的字段。

  • Last-Modified:Wed, 28 Sep 2016 09:24:35 GMT,这表示这个文件最后的修改时间是2016年9月28日9点24分35秒。这个字段对于浏览器来说,会在下次请求的时候,作为Request Header的If-Modified-Since字段带上。例如浏览器缓存的文件已经超过了Cache-Control(或者Expires),那么需要加载这个文件时,就会发出请求,请求的Header有一个字段为If-Modified-Since:Wed, 28 Sep 2016 09:24:35 GMT,服务器接收到请求后,会把文件的Last-Modified时间和这个时间对比,如果时间没变,那么浏览器将返回304 Not Modified给浏览器,且content-length肯定是0个字节。如果时间有变化,那么服务器会返回200 OK,并返回相应的内容给浏览器。

  • ETag:”57eb8c5c-129”,这是文件的特征串。功能同上面的Last-Modified是一样的。只是在浏览器下次请求时,ETag是作为Request Header中的If-None-Match:"57eb8c5c-129"字段传到服务器。服务器和最新的文件特征串对比,如果相同那么返回304 Not Modified,不同则返回200 OK。当ETag和Last-Modified同时出现时,任何一个字段只要生效了,就认为文件是没有更新的。

2.WebView如何设置才能支持上面的协议

由上面的介绍可知,只要是个主流的、合格的浏览器,都应该能够支持HTTP协议层面的这几个字段。这不是我们开发者可以修改的,也不是我们应该修改的配置。在Android上,我们的WebView也支持这几个字段。但是我们可以通过代码去设置WebView的Cache Mode,而使得协议生效或者无效。WebView有下面几个Cache Mode:

  • LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据。
  • LOAD_DEFAULT: 根据cache-control决定是否从网络上取数据。
  • LOAD_CACHE_NORMAL: API level 17中已经废弃,从API level 11开始作用同LOAD_DEFAULT模式
  • LOAD_NO_CACHE: 不使用缓存,只从网络获取数据。
  • LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。本地没有缓存时才从网络上获取。

设置WebView缓存的Cache Mode示例代码如下:

1

2

WebSettings settings = webView.getSettings();

settings.setCacheMode(WebSettings.LOAD_DEFAULT);

网上很多人都说根据网络条件去选择Cache Mode,当有网络时,设置为LOAD_DEFAULT,当没有网络时设置为LOAD_CACHE_ELSE_NETWORK。但是在我的业务中,js文件的更新都是非覆盖式的更新,也就是时候每次改变js文件的时候,文件的url地址一定会发生变化,所以我希望浏览器能够缓存下来js,并且一直使用它,那么我就给它只设置为LOAD_CACHE_ELSE_NETWORK。当然如果你要是可以改js的cdn服务器的Cache-Control字段,那也行啊,用LOAD_DEFAULT就ok了。至于文件是应该采用覆盖式or非覆盖式的更新,不是我今天要讨论的内容,在web前端领域,这是一个可以聊聊的topic。

关于iOS的WebView,我同事在实际测试的时候竟然发现,控制文件缓存的Response Header是Expires字段。。而且iOS无法针对整个WebView设置Cache Mode,只能针对每一个URLRequest去设置。。后续有机会要学习一下iOS那块的情况。

3.在手机里面的存储路径

浏览器默认缓存下来的文件是怎么被存储到了哪里呢?这个问题在接触到WebView以来,就一直是一个谜题。这次由于工作的需要,我特意root了两台手机,一台红米1(Android 4.4)和一台小米4c(Android 5.1),在root高系统版本(6.0和7.1)的两台Nexus都以失败告终之后,我决定还是先看看4.4和5.1系统上,WebView自带的缓存存到了哪里。

首先,不用思考就知道,这些文件一定是在/data/data/包名/目录下,在我之前的一篇博客里面提到过,这是每一个应用自己的内部存储目录。

接着,我们打开终端,使用adb连接手机,然后按照下面命令操作一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.先进入shell

adb shell

// 2.开启root账号

su

// 3.修改文件夹权限

chmod 777 data/data/你的应用包名/

// 4.修改子文件夹的权限,因为Android命令行不支持向Linux那样的-R命令实现递归式的chmod。。。

chmod 777 data/data/你的应用包名/*

// 5.所以如果你对应用目录层级更深,你就要进一步地chmod。。。

chmod 777 data/data/你的应用包名/*/*

// 6.直到终端里提示你说,no such file or directory时,说明chmod完了,所有的内部存储里面的文件夹和文件都可以看到了,如果大家有更好的方法请一定告诉我,多谢了~
  • Android 4.4的目录:/data/data/包名/app_webview/cache/,如下图所示的第二个文件夹。

Android4.4系统WebView自带缓存路径

可能你注意到了,第一个文件夹是叫Application Cache,我们后面再说它。

  • Android 5.1的目录:/data/data/包名/cache/org.chromium.android_webview/下面,如下图所示。

但是在5.1系统上,/data/data/包名/app_webview/文件夹依然存在,只是4.4系统上面存储WebView自带缓存的app_webview/cache文件夹不再存在了(注意下App Cache目录还在),如下图所示。

Android5.1系统WebView自带缓存路径

综上所述,WebView自带的浏览器协议支持的缓存,在不同的系统版本上,位置是不一样的。也许除了我root过的4.4、5.1以外,其他版本系统的WebView自带缓存还可能存在于不同的目录里面。

另外一个是关于缓存文件的存储格式和索引格式,在不同的手机上可能也有差别,因为之前看到网上的人都说有叫webview.db或者webviewCache.db的文件,这个文件呢,还不是在app_webview/cache或者org.chromium.android_webview下面,而是在/data/data/包名/database/里面。但是,我这两台root过的手机都没有看到这种文件,而且我把/data/data/包名/下面所有的db文件都打开看了,并没有发现有存储url记录的table。。

实际上,以5.1系统为例,我看到了/data/data/包名/cache/org.chromium.android_webview/下面有叫index/index-dir/the-real-index的文件,以及一堆名称为md5+下划线+数字的文件,上面的图中也可以看得到,这块的原理仍然有些疑问,也希望专业的大神可以解答一下。

四、H5的缓存

讲完了WebView自带的缓存,下面讲一下H5里面的App Cache。这个Cache是由开发Web页面的开发者控制的,而不是由Native去控制的,但是Native里面的WebView也需要我们做一下设置才能支持H5的这个特性。

1.工作原理

写Web页面代码时,指定manifest属性即可让页面使用App Cache。通常html页面代码会这么写:

1
2
3
4

<html manifest="xxx.appcache">

</html>

xxx.appcache文件用的是相对路径,这时appcache文件的路径是和页面一样的。也可以使用的绝对路径,但是域名要保持和页面一致。

完整的xxx.appcache文件一般包括了3个section,基本格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

CACHE MANIFEST

\# 2017-05-13 v1.0.0

/bridge.js

NETWORK:

*

FALLBACK:

/404.html
  • CACHE MANIFEST下面文件就是要被浏览器缓存的文件
  • NETWORK下面的文件就是要被加载的文件
  • FALLBACK下面的文件是目标页面加载失败时的显示的页面

AppCache工作的原理:当一个设置了manifest文件的html页面被加载时,CACHE MANIFEST指定的文件就会被缓存到浏览器的App Cache目录下面。当下次加载这个页面时,会首先应用通过manifest已经缓存过的文件,然后发起一个加载xxx.appcache文件的请求到服务器,如果xxx.appcache文件没有被修改过,那么服务器会返回304 Not Modified给到浏览器,如果xxx.appcache文件被修改过,那么服务器会返回200 OK,并返回新的xxx.appcache文件的内容给浏览器,浏览器收到之后,再把新的xxx.appcache文件中指定的内容加载过来进行缓存。

可以看到,AppCache缓存需要在每次加载页面时都发出一个xxx.appcache的请求去检查manifest文件是不是有更新(byte by byte)。根据这篇文章(H5 缓存机制浅析 移动端 Web 加载性能优化)的介绍,AppCache有一些坑的地方,且官方已经不推荐使用了,但目前主流的浏览器依然是支持的。文章里主要提到下面这些坑:

  • 要更新缓存的文件,需要更新包含它的 manifest 文件,那怕只加一个空格。常用的方法,是修改 manifest 文件注释中的版本号。如:# 2012-02-21 v1.0.0
  • 被缓存的文件,浏览器是先使用,再通过检查 manifest 文件是否有更新来更新缓存文件。这样缓存文件可能用的不是最新的版本。
  • 在更新缓存过程中,如果有一个文件更新失败,则整个更新会失败。
  • manifest 和引用它的HTML要在相同 HOST。
  • manifest 文件中的文件列表,如果是相对路径,则是相对 manifest 文件的相对路径。
  • manifest 也有可能更新出错,导致缓存文件更新失败。
  • 没有缓存的资源在已经缓存的 HTML 中不能加载,即使有网络。例如:[url=]http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/[/url]
  • manifest 文件本身不能被缓存,且 manifest 文件的更新使用的是浏览器缓存机制。所以 manifest 文件的 Cache-Control 缓存时间不能设置太长。

2.WebView如何设置才能支持AppCache

WebView默认是没有开启AppCache支持的,需要添加下面这几行代码来设置:

1
2
3
4
5
6
7
8
9
10

WebSettings webSettings = webView.getSettings();

webSettings.setAppCacheEnabled(true);

String cachePath = getApplicationContext().getCacheDir().getPath(); // 把内部私有缓存目录'/data/data/包名/cache/'作为WebView的AppCache的存储路径

webSettings.setAppCachePath(cachePath);

webSettings.setAppCacheMaxSize(5 * 1024 * 1024);

注意:WebSettings的setAppCacheEnabled和setAppCachePath都必须要调用才行。

3.存储AppCache的路径

按照Android SDK的API说明,setAppCachePath是可以用来设置AppCache路径的,但是我实际测试发现,不管你怎么设置这个路径,设置到应用自己的内部私有目录还是外部SD卡,都无法生效。AppCache缓存文件最终都会存到/data/data/包名/app_webview/cache/Application Cache这个文件夹下面,在上面的Android 4.4和5.1系统目录截图可以看得到,但是如果你不调用setAppCachePath方法,WebView将不会产生这个目录。这里有点让我觉得奇怪,我猜测可能从某一个系统版本开始,为了缓存文件的完整性和安全性考虑,SDK实现的时候就吧AppCache缓存目录设置到了内部私有存储。

五、总结

相同点

WebView自带的缓存和AppCache都是可以用来做文件级别的缓存的,基本上比较好地满足对于非覆盖式的js、css等文件更新。

不同点

  • WebView自带的缓存是是协议层实现的(浏览器内核标准实现,开发者无法改变);而AppCache是应用层实现的。
  • WebView的缓存目录在不同系统上可能是不同的;而对于AppCache而言,AppCache的存储路径虽然有方法设置,但是最终都存储到了一个固定的内部私有目录下。
  • WebView自带的缓存可以在缓存生效的时候不用再发HTTP请求;而AppCache一定会发出一个manifest文件的请求。
  • WebView自带的缓存可以通过设置CacheMode来改变WebView的缓存机制;而AppCache的缓存策略是由manifest文件控制的,也就是说是由web页面开发者控制的。

最后说一下,其实很多时候,这两类缓存是共同在工作的,当manifest文件没有控制某些资源加载时,例如我上面写的xxx.appcache文件里,NETWORK section下面用的是*号,意思是所有不缓存的文件都要去网络加载。此时,这些资源就会走到WebView自带的缓存机制去,结合WebView的CacheMode,我们实际上对这些文件进行了一次WebView自带的缓存。搞清楚这两类缓存的原理有利于我们更好的设计自己的页面和App,尽可能减少网络请求,提高App运行效率。

# Android # WebView

python利用beautifulsoup+selenium自动翻页抓取网页内容

使用React.js开发Chrome插件

什么是泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。


假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案是可以使用 Java 泛型。

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

泛型的分类

根据泛型的使用场景,一般可以分成三类,分别是:泛型类泛型方法和泛型接口

泛型类型擦出

Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
用代码来说明就是:

1
2
3
4
5
6
7
8
9
10
11
public class test {

public static void main(String args[]){
List<Integer> a = new ArrayList<>();
List<String> b = new ArrayList<>();
System.out.println(a.getClass().toString());
System.out.println(a.getClass().toString());

System.out.println(b.getClass().equals(a.getClass()));
}
}

运行的结果如下:

1
2
3
class java.util.ArrayList
class java.util.ArrayList
true

可以看到,List<Integer>List<String>的类是相同的。

总结成一句话就是:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

泛型类

泛型类的使用非常简单,在类名添加<T> 或者类似于这类的泛型类声明即可。

1
2
3
4
5
6
7
8
9
10
11
class Person<T>{
private T t;

public Person(T t) {
this.t = t;
}

public T getKey(){
return t;
}
}

我们在使用这个泛型类的时候,就可以传入不同类型的参数来进行初始化,可以显示声明数据类型,也可以不声明,jvm会自动帮我们声明。使用实例代码如下:

1
2
3
4
5
6
7
8
9
Person p1 = new Person<Integer>(12345);
Person p2 = new Person<Double>(8989.9);
Person p3 = new Person<>(true);
Person p4 = new Person<>("i am ok!");

System.out.println("p1 -> "+p1.getKey());
System.out.println("p2 -> "+p2.getKey());
System.out.println("p3 -> "+p3.getKey());
System.out.println("p4 -> "+p4.getKey());

输出的结果如下:

1
2
3
4
p1 -> 12345
p2 -> 8989.9
p3 -> true
p4 -> i am ok!

可以看到,使用泛型类非常简单,只需要在定义类的时候添加<T><K><V>这类的泛型类的定义即可。当然,如果有需要,我们可以限制其使用范围,例如<T extends Number> 就规定了传入的类型是只能是Number的子类。

泛型方法

泛型方法的规则如下:

  1. 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
  2. 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  3. 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  4. 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

泛型方法与泛型类的定义类似,泛型类的定义是在类名后加<T>这类的泛型修饰词,泛型方法的定义就是在方法的返回值添加<T>这类的修饰词。先来看泛型方法的定义示例:

1
2
3
private <T> void helloPerson(T t){
System.out.println("t = [" + t + "]");
}

注意,这里我们称他为泛型方法是因为void前面的<T>,而不是helloPerson(T t)中的T
我们在泛型类的Person中的public T getKey() 就不能算一个泛型方法,因为这里的T只是getKey方法的一个返回值而已。
然后泛型方法的使用也很简单:

1
2
3
4
helloPerson(123);
helloPerson(12.3);
helloPerson(true);
helloPerson("i am ok");

上面方法的运行结果如下:

1
2
3
4
t = [123]
t = [12.3]
t = [true]
t = [i am ok]

泛型接口

泛型接口的定义同样很简单,只需要在接口的名字后加上<T>这类的泛型定义接口。例如下面是一个泛型接口的示例代码:

1
2
3
public interface IFunction<T> {
void doSomeThing(T t);
}

在泛型接口里面定义了泛型,我们就可以在对应的方法中使用其类型。
当然,接口的定义好之后还是需要去实现这个接口的,实现的方式有两种,一种是声明类型,一种是不声明类型。
下面是第一种,申明类型的实现:

1
2
3
4
5
6
7
public class Eat implements IFunction<String> {

@Override
public void doSomeThing(String s) {
System.out.println("s = [" + s + "]");
}
}

可以看到,在Eat的实现中,我们讲泛型显示声明成了String,如此我们在使用Eat类中的doSomeThing方法时,就只能传入String类型的。
另一种,是不声明具体类型的实现:

1
2
3
4
5
6
7
public class Read<T> implements IFunction<T> {

@Override
public void doSomeThing(T t) {
System.out.println("t = [" + t + "]");
}
}

可以看到,其实Read就已经被声明成了一个泛型类,因为实现了泛型接口,我们要继续使用泛型来实现的话,就需要将类实现成泛型类。
我们来看其调用:

1
2
3
4
5
6
7
8
9
10
11
IFunction f1 = new Eat();
IFunction f2 = new Read();

f1.doSomeThing("f1-> only eat!");
//f1.doSomeThing(123456);
//这句编译不会报错,运行会抛出java.lang.Integer cannot be cast to java.lang.String 异常

f2.doSomeThing(132);
f2.doSomeThing("read");
f2.doSomeThing(true);
f2.doSomeThing(52.1);

输出的结果如下:

1
2
3
4
5
s = [f1-> only eat!]
t = [132]
t = [read]
t = [true]
t = [52.1]

可以看到,我们在继承泛型接口时,显示申明了其类型之后,我们调用时也只能使用对应的类型,如果传入其他的类型,编译器并不会报错,但是运行时会抛出 类型强转失败的异常 。

泛型通配符

类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类。注意,?可以代表String,Interger等其他具体的类型,但它是不确定的,而<T>表示一个类型参数,不能当作实际参数使用。说白了,就是?,表示不确定的类型,和StringInteger这种类型一样,是实实在在的类型,而T,只是表示类型参数,在使用时还是会转化成对应的实际参数的。
使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13

public class TypeTest {
public static void main(String args[]){
TypeTest typeTest = new TypeTest();
typeTest.helloMan(Arrays.asList("1","hello"));
typeTest.helloMan(Arrays.asList(1,2,3));

}

private void helloMan(List<?> a){
System.out.println("a = [" + a + "]");
}
}

可以看到,我往同一个方法出入不同类型的参数也时可以正常运行的,输出结果为:

1
2
a = [[1, hello]]
a = [[1, 2, 3]]

还有,补充两点;

  1. <? extends T>表示该通配符所代表的类型是T类型的子类。
  2. <? super T>表示该通配符所代表的类型是T类型的父类

参考链接

java中的反射

在java中,有一种可以跨越常规语法的用法,它就是反射。通过反射,我们可以访问到我们原本访问不到的类和对象,也不能调用我们原本调用不到的方法。虽然反射很强大,但是它违反了程序设计的初衷,且用反射的性能会有一定的影响,所以我们应该避免使用反射。但是反射功能实在强大,正确的使用能使得我们的代码变得很简洁,所以我们还是有必要学习一下反射的相关知识。

反射的原理

深入分析Java方法反射的实现原理

获取类实例

获取类实例一般有两种写法,一种是类名.class,另一种则是class.forName()。假如我们已经有了一个Person的类,那么我们可以通过如下三种方式来获取类实例。

1
2
3
4
5
6
//方式1
Class person1 = Person.class;
//方式2
Class person2 = Class.forName("day18.Person");
//方式3,如果能用这种肯定用这种!
Person person3 = new Person();

两种方式的区别是,一种是我们能直接拿到那个类对象,另一种是我们无法中拿到类对象,必须通过反射才能获取到类对象。

通过反射获取构造方法

假设我们有这个一个Person类,代码如下:

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
class Person {
public String name;
public char sex;
public int height;
protected float id;
private String des;

private Person() {
this.name = "jone";
this.sex = 'f';
this.height = 155;
this.id = 420458188603268922F;
this.des = "just a normal person";
}

private Person(String name) {
this.name = name;
}

private Person(String name, char sex) {
this.name = name;
this.sex = sex;
}

private Person(String name, char sex, int height) {
this.name = name;
this.sex = sex;
this.height = height;
}
}

那么我们如何去创建对应的Person对象呢,不能因为我们没有对象就不创建对象呀!答案是我们可以利用反射获得对应的构造方法,然后利用newInstance()方法,即可创建对应的Person对象。例如我们可以这么写,来获得对应的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 try {
//获取对应的类对象,如果直接获取不到,可以用Class.forName("packageName.Person"); 来获取
Class person1 = Person.class;
//获取无参的构造方法
Constructor constructor = person1.getDeclaredConstructor();
//设置属性为可访问,因为其构造方法是private的,所以这句必不可少,
//否则会报 can not access a member of class day18.Person with modifiers "private" 的错误
constructor.setAccessible(true);
//利用newInstance 创建对应的对象
Person p1 = (Person) constructor.newInstance();

Constructor constructor2 = person1.getDeclaredConstructor(String.class);
constructor2.setAccessible(true);
Person p2 = (Person) constructor2.newInstance("p2");

System.out.println(p1.name);
System.out.println(p2.name);
} catch (Exception e) {
e.printStackTrace();
}

输出如下:

1
2
jone
p2

我们可以利用getDeclaredConstructor(),来获取对应的构造方法。当然,还有其他方法可以获取到构造方法,我们这里以他为例。getDeclaredConstructor(),中间可以添加任意个参数,表示有多少个参数的构造方法。例如getDeclaredConstructor(String.class) ,对应的构造方法就是private Person(String name),然后在newInstance,传入对应的参数即可。

获取构造方法的四个方法

方法名 作用 备注
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 获得对应的构造方法 获取的构造方法必须是当前类申明的,无法获取到父类的构造方法
public Annotation[] getDeclaredAnnotations() 获得所有的构造方法 获取所有的构造方法都是当前类申明的,无法获取到父类的构造方法
public Constructor<T> getConstructor(Class<?>... parameterTypes) 获得指定的构造方法 获得的构造方法必须是public的,可以获取从父类继承的构造方法
public Constructor<?>[] getConstructors() 获得所有的构造方法 获取构造方法都是public的,可以获取到父类的phblic构造方法

通过反射获取属性

还是沿用上次的Person类,我们这次来获取他的des属性。观察一下,发现des属性为private,常规就无法获取到其属性值,下面我们通过反射来获取其des属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 try {
//获取对应的类对象,如果直接获取不到,可以用Class.forName("packageName.Person"); 来获取
Class person1 = Person.class;
//获取无参构造方法
Constructor constructor = person1.getDeclaredConstructor();
//设置构造方法可访问
constructor.setAccessible(true);
//创建Person对象
Person p1 = (Person) constructor.newInstance();
//获取des 属性值
Field des = person1.getDeclaredField("des");
//设置 des 可访问
des.setAccessible(true);


System.out.println("des= "+des.get(p1));

} catch (Exception e) {
e.printStackTrace();
}

注释很详细,不在赘述了,这里打印的结果就是Person类里无参构造方法赋值的:

1
des= just a normal person

获取属性的方法

方法名 作用 备注
public Field[] getDeclaredFields() 获取所有的属性 只能获取本类的属性(包括private的),不能获取到父类的属性
public Field getDeclaredField(String name) 获取指定的的属性 只能到获取本类的属性(包括private的),不能获取到父类的属性
public Field[] getFields() 获取所有的属性 只能到获取public的属性,也可以获取到父类的public属性
public Field getField(String name) 获取指定的的属性 只能到获取public的属性,也可以获取到父类的public属性

通过反射获取方法

我们还是是假设有一个Person类,我们将其改造成如下:

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
class Person {
public String name;
public char sex;
public int height;
protected float id;
private String des;

private Person() {
this.name = "jone";
this.sex = 'f';
this.height = 155;
this.id = 420458188603268922F;
this.des = "just a normal person";
}


private void showInfo(){
System.out.println("name="+name);
System.out.println("sex="+sex);
System.out.println("height="+height);
System.out.println("id="+id);
System.out.println("des="+des);
}

private void showInfo(String name){
System.out.println("name = [" + name + "]");
}

private String showInfo(String name,String des){
return "the people name is"+name+" and des is "+des;
}

}

如上,我们实现了三个ShowInfo()的重载方法,但是都是private类型的,一般情况下在外部是无法调用的,下面我们通过反射来进行调用。

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
try {
//获取对应的类对象,如果直接获取不到,可以用Class.forName("packageName.Person"); 来获取
Class person1 = Person.class;
//获取无参构造方法
Constructor constructor = person1.getDeclaredConstructor();
//设置构造方法可访问
constructor.setAccessible(true);
//创建Person对象
Person p1 = (Person) constructor.newInstance();

//获取无参的showInfo方法
Method method1 = person1.getDeclaredMethod("showInfo");
//设置showInfo方法访问
method1.setAccessible(true);
//执行无参的showInfo方法
method1.invoke(p1);
System.out.println();

//获取一个参数的的showInfo方法
Method method2 = person1.getDeclaredMethod("showInfo",String.class);
//设置showInfo方法访问
method2.setAccessible(true);
//执行无参的showInfo方法
method2.invoke(p1,"call from outside:");
System.out.println();

//获取两个参数的的showInfo方法
Method method3 = person1.getDeclaredMethod("showInfo",String.class,String.class);
//设置showInfo方法访问
method3.setAccessible(true);
//执行无参的showInfo方法
String dsc = (String) method3.invoke(p1," arche","call from outside:");
System.out.println(dsc);

} catch (Exception e) {
e.printStackTrace();
}

我们通过反射分别调用了上述的三个参数,输出的结果如下:

1
2
3
4
5
6
7
8
9
name=jone
sex=f
height=155
id=4.20458194E17
des=just a normal person

name = [call from outside:]

the people name is arche and des is call from outside:

下面总结一下反射方法相关的函数:

通过反射获取参数

方法名 作用 备注
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) 获取指定的方法 只能获取本类的方法(包括private的),不能获取到父类的方法
public Field[] getDeclaredFields() 获取所有的的方法 只能获取本类的方法(包括private的),不能获取到父类的方法
public Method getMethod(String name, Class<?>... parameterTypes) 获取指定的的方法 只能获取public的方法,也能获取到父类的public方法
public Method[] getMethods() 获取所有的的方法 只能获取public的方法,能获取到父类的public方法
public Object invoke(Object obj, Object... args) 执行对应的方法 执行方法的参数应和获取方法时的参数类型相同,可以为null
public Class<?> getReturnType() 获取方法的返回值类型 使用示例Class<?> type = method1.getReturnType();
public Class<?>[] getParameterTypes() 获得方法的传入参数类型 使用示例Class<?>[] Parametertypes = method1.getParameterTypes();
public Class<?>[] getParameterTypes() 获得方法的传入参数类型 使用示例Class<?>[] Parametertypes = method1.getParameterTypes();

通过反射获取注解

通过反射获取泛型

假设Person对象如下所示:

1
2
3
class Person {
private List<String> friends;
}

我们要获取其 friends对象的泛型,可以用下面的方式实现:

1
2
3
4
5
6
7
8
9
10
11
 try {
Class<?> p = Person.class;
Field friends = p.getDeclaredField("friends");
Type genericType = friends.getGenericType();//获取对应的类型
ParameterizedTypeImpl parameterizedType = (ParameterizedTypeImpl) genericType; //强转成具体的实现类
Type[] genericTypes = parameterizedType.getActualTypeArguments(); //取得包含的泛型类型
System.out.println(genericTypes[0]);

} catch (Exception e) {
e.printStackTrace();
}

如此我们就获取的了friends的泛型类,打印的结果为:

class java.lang.String

参考链接

代理模式

什么是代理模式?代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
下面以一个简单的例子来说明代理模式。
小明有两件事要做,用代码表示为:

1
2
3
4
public interface IDoSomething {
String[] doShopping(int money);
Object doOtherThing(String thing);
}

当然,小明要做这些事情,他也知道怎么去做,购物和做其他事情的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DoSomethingImpl implements IDoSomething {

@Override
public String[] doShopping(int money) {
System.out.println("买买买!!");
System.out.println(String.format("花费%d $",money));
return new String[]{"零食","游戏机","mac"};
}

@Override
public Object doOtherThing(String thing) {
System.out.println(" Other Thing ->"+thing);

return thing+" is done";
}
}

虽然,小明有这两件事要做,他也知道怎么去做,但是他现在就是不想自己去做。那怎么办?
找人帮他做呗,也就是找个代理,代理的简单实现如下:

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
public class ProxyDoSomething implements IDoSomething{
private IDoSomething base;

public ProxyDoSomething(IDoSomething base) {
this.base = base;
}


@Override
public String[] doShopping(int money) {
//先扣点手续费
int realMoney = (int) (money*0.1);
String [] object = base.doShopping(realMoney);
if (object != null && object.length > 1){
object[0]= "把这个换成拼多多的货";
}
return object;
}

@Override
public Object doOtherThing(String thing) {
String str = thing+" 这件事是代理出面做的";
String res = (String) base.doOtherThing(str);
return res;
}
}

找人做代理,难免要点手续费,然后可能还会动点手脚。当然,最终事情还是帮我们做好了,接下来我们看不要小明亲自出面,怎么来实现的。

1
2
3
4
5
6
public static void main(String args[]){
IDoSomething iDoSomething = new DoSomethingImpl();
ProxyDoSomething proxyDoSomething = new ProxyDoSomething(iDoSomething);
System.out.print(proxyDoSomething.doOtherThing(" clean house").toString());
System.out.println(Arrays.toString(proxyDoSomething.doShopping(1000)));
}

可以看到,本来是小明要做的事情,最终代理给了ProxyDoSomething去完成,当然,代价也比较惨重。
最终打印的结果为:

1
2
3
4
 Other Thing -> clean house 这件事是代理出面做的
clean house 这件事是代理出面做的 is done买买买!!
花费100 $
[把这个换成拼多多的货, 游戏机, mac]

动态代理

通过前一个例子理解了什么是代理,那么什么是动态代理呢?
通俗的理解就是,这个代理的对象可以代理很多事情,不会局限于静态代理一对一的关系,动态代理可以实现一 对多。类似于一个万能的代理,既能给代理购物,也能给别人代理做家务等等。
废话不多说,先上代理的核心代码:

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
* 动态代理实现的核心,很多事情就是在这里进行实现的。
* 首先,我们需要拿到需要代理的对象
* 然后,需要判断对应的方法
* 最后,通过反射进行调用
*/
public class DoSomethingHandler implements InvocationHandler {
private IDoSomething doSomething;

public DoSomethingHandler(IDoSomething doSomething) {
this.doSomething = doSomething;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("doShopping".equals(method.getName())){
int money = (int) ((int) args[0]*0.8);
doSomething.doShopping(money);
String[] things = (String[]) method.invoke(doSomething, money);
if (things != null && things.length > 1){
things[0] = "这个我要掉包,换个来自拼多多的好货";
}
return things;
}else if("doOtherThing".equals(method.getName())){
String str = args[0] +" by DosomeThingHadler";
Object o = method.invoke(doSomething,str);
return o+" 来自 DosomeThingHadler 的问候";
}
return null;
}
}

简单的介绍在代码里进行介绍了,这里就不多介绍了。我们来看调用的方法。

1
2
3
4
5
6
7
8
9
System.out.println("开始啦啦啦啦~ ");
IDoSomething people = new DoSomethingImpl();
//不使用代理,自己来
System.out.println(Arrays.toString(people.doShopping(1000)));
//动态代理
people = (IDoSomething) Proxy.newProxyInstance(IDoSomething.class.getClassLoader(),people.getClass().getInterfaces()
,new DoSomethingHandler(people));
System.out.println(Arrays.toString(people.doShopping(1000)));
System.out.print(people.doOtherThing(" clean house"));

通过Proxy.newProxyInstance创建出动态代理实例,然后进行调用即可。
上例运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
开始啦啦啦啦~ 
买买买!!
花费1000 $
[零食, 游戏机, mac]

买买买!!
花费800 $
买买买!!
花费800 $
[这个我要掉包,换个来自拼多多的好货, 游戏机, mac]
Other Thing -> clean house by DosomeThingHadler
clean house by DosomeThingHadler is done 来自 DosomeThingHadler 的问候

总结

动态代理的实现主要是基于Proxy.newProxyInstance创建出我们需要的代理对象,而创建这个对象又依赖于一个InvocationHandler,通过他来实现我们要代理的事情。

参考链接

Android插件化原理解析——概要

发表于 2016-01-28 | 63874次阅读

2015年是Android插件化技术突飞猛进的一年,随着业务的发展各大厂商都碰到了Android Native平台的瓶颈:

  1. 从技术上讲,业务逻辑的复杂导致代码量急剧膨胀,各大厂商陆续出到65535方法数的天花板;同时,运营为王的时代对于模块热更新提出了更高的要求。
  2. 在业务层面上,功能模块的解耦以及维护团队的分离也是大势所趋;各个团队维护着同一个App的不同模块,如果每个模块升级新功能都需要对整个app进行升级,那么发布流程不仅复杂而且效率低下;在讲究小步快跑和持续迭代的移动互联网必将遭到淘汰。

H5和Hybird可以解决这些问题,但是始终比不上native的用户体验;于是,国外的FaceBook推出了react-native;而国内各大厂商几乎都选择纯native的插件化技术。可以说,Android的未来必将是react-native和插件化的天下。

react-native资料很多,但是讲述插件化的却凤毛菱角;插件化技术听起来高深莫测,实际上要解决的就是两个问题:

  1. 代码加载
  2. 资源加载

代码加载

类的加载可以使用Java的ClassLoader机制,但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理

另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?这就是ClassLoader的管理问题

资源加载

资源加载方案大家使用的原理都差不多,都是用AssetManager的隐藏方法addAssetPath;但是,不同插件的资源如何管理?是公用一套资源还是插件独立资源?共用资源如何避免资源冲突?对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt要么添加编译插件);有的方案选择独立资源,不同插件管理自己的资源。

目前国内开源的较成熟的插件方案有DLDroidPlugin;但是DL方案仅仅对Frameworl的表层做了处理,严重依赖that语法,编写插件代码和主程序代码需单独区分;而DroidPlugin通过Hook增强了Framework层的很多系统服务,开发插件就跟开发独立app差不多;就拿Activity生命周期的管理来说,DL的代理方式就像是牵线木偶,插件只不过是操纵傀儡而已;而DroidPlugin则是借尸还魂,插件是有血有肉的系统管理的真正组件;DroidPlugin Hook了系统几乎所有的Sevice,欺骗了大部分的系统API;掌握这个Hook过程需要掌握很多系统原理,因此学习DroidPlugin对于整个Android FrameWork层大有裨益。

接下来的一系列文章将以DroidPlugin为例讲解插件框架的原理,揭开插件化的神秘面纱;同时还能帮助深入理解Android Framewrok;主要内容如下:

另外,对于每一章内容都会有详细的demo,具体见understand-plugin-framework;喜欢就点个关注吧~定期更新,敬请期待!

#android #droidplugin #plugin framework

Android插件化原理解析——Hook机制之动态代理

你真的了解AsyncTask?

37 日志

RSS

Creative Commons

  1. 1. 代码加载
  2. 2. 资源加载

效果预览

效果预览

本次的图片效果是基于上一篇博客的三种方法进行扩展实现图片的外边框效果。上一篇博客的传送门
下面开始进行到整体,针对上次实现圆形图片的三个方式添加一个外边框效果。

使用PorterDuffXfermode方式来实现

我们使用这个方式实现圆形图片的原理是通过图片的设置图片的叠加显示效果。还是这张图:
官方的说明图

思路

实现圆形图片,我们只需要绘制一个圆,并将模式设置为SRC_IN即可。但是我们需要在外围添加一个边框的话,就不会那么简单了。我们需要绘制三次图片,通过合理的设置他们的Xfermode模式来达到我们的要求。具体步骤为:

  1. 将原图片绘制出来(如果有需要,进行必要的比例缩放处理,使其是一个正方形)
  2. 绘制一个等大的正方形,颜色为边框要显示的颜色。(这一步的目的是为了将外围的边框也绘制出来)
  3. 绘制一个半径为二分之一边长减去边框长度的圆,模式为DST_OUT
  4. 绘制一个半径为二分之一边长的圆,模式为DST_IN
    通过上面四步,基本就实现了我们想要的外边框的效果。
    用图表示为:

    具体细节可以参考代码实现。

    代码

    自定义viewCircleImageView1的代码如下。
    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
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    public class CircleImageView1 extends ImageView {

    public static final int DEFAULT_BORDER_WIDTH = 0;

    private Paint mPaint;
    private Paint mBorderPaint;
    private Shape mShape;
    private Shape mBorderShape;
    private float mRadius;
    private float mBorderWidth;
    private int mBorderColor;
    private PorterDuffXfermode mBorderDuffMode;
    private Bitmap mBorderBitmap;

    private float[] outerRadii = new float[8];

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

    public CircleImageView1(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public CircleImageView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView(attrs);
    }

    private void initView(AttributeSet attrs) {
    if (attrs != null) {
    TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircleImageView1);
    mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView1_circle_border_width1, DEFAULT_BORDER_WIDTH);
    mBorderColor = a.getInt(R.styleable.CircleImageView1_circle_border_color1, Color.BLACK);
    a.recycle();
    }
    setLayerType(LAYER_TYPE_HARDWARE, null);
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.BLACK);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));

    if (mBorderWidth != DEFAULT_BORDER_WIDTH) {
    mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mBorderPaint.setColor(mBorderColor);
    mBorderPaint.setFilterBitmap(true);
    mBorderDuffMode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
    }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    int min = Math.min(width, height);
    mRadius = min / 2;
    setMeasuredDimension(min, min);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (changed) {
    if (mShape == null) {
    Arrays.fill(outerRadii, mRadius);
    mShape = new RoundRectShape(outerRadii, null, null);
    if (mBorderWidth != DEFAULT_BORDER_WIDTH && mBorderShape == null) {
    mBorderShape = new RoundRectShape(outerRadii, null, null);
    }

    }
    mShape.resize(getWidth(), getHeight());
    if (mBorderWidth != DEFAULT_BORDER_WIDTH && mBorderShape != null) {
    mBorderShape.resize(getWidth() - mBorderWidth * 2, getHeight() - mBorderWidth * 2);
    makeStrokeBitmap();
    }
    }
    }

    @Override
    protected void onDraw(Canvas canvas) {
    int saveCount = canvas.getSaveCount();
    canvas.save();
    super.onDraw(canvas);
    if (mBorderWidth != DEFAULT_BORDER_WIDTH && mBorderShape != null && mBorderBitmap != null) {
    int i = canvas.saveLayer(0, 0, getMeasuredWidth(), getMeasuredHeight(), null, Canvas.ALL_SAVE_FLAG);
    //1.绘制外边框颜色的正方形
    mBorderPaint.setXfermode(null);
    canvas.drawBitmap(mBorderBitmap, 0, 0, mBorderPaint);
    //2.适当画布的中心位置
    canvas.translate(mBorderWidth, mBorderWidth);
    //3.绘制减去边框长度的图片,模式为DST_OUT
    mBorderPaint.setXfermode(mBorderDuffMode);
    mBorderShape.draw(canvas, mBorderPaint);
    canvas.restoreToCount(i);
    }

    if (mShape != null) {
    mShape.draw(canvas, mPaint);
    }
    canvas.restoreToCount(saveCount);
    }

    private void makeStrokeBitmap() {
    if (mBorderWidth <= 0) return;

    int w = getMeasuredWidth();
    int h = getMeasuredHeight();

    if (w == 0 || h == 0) {
    return;
    }

    releaseBorderBitmap();

    mBorderBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(mBorderBitmap);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
    p.setColor(mBorderColor);
    c.drawRect(new RectF(0, 0, w, h), p);
    }

    private void releaseBorderBitmap() {
    if (mBorderBitmap != null) {
    mBorderBitmap.recycle();
    mBorderBitmap = null;
    }
    }


    public void build(String url) {
    Glide.with(getContext()).load(url).into(this);
    }
    }
    在代码中使用到了自定义的属性,所以需要在value/attrs文件中添加支持,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <declare-styleable name="CircleImageView1">
    <attr name="circle_border_width1" format="dimension" />
    <attr name="circle_border_color1" format="color" />
    <attr name="circle_border_overlay1" format="boolean" />
    <attr name="circle_fill_color1" format="color" />
    <attr name="circle_circle_background_color1" format="color" />
    </declare-styleable>
    </resources>
    使用也很简单,在xml中使用的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    <com.example.cm.circleview.view2.CircleImageView1
    android:id="@+id/image2_id1"
    app:circle_border_width1="5dp"
    app:circle_border_color1="#fff"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_marginTop="10dp"
    android:layout_marginBottom="10dp" />
    通过java代码设置图片来源,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class MainActivity extends AppCompatActivity {

    public static final String URL = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533911636&di=04ec95ec44a1623d52b20ff08727ac78&imgtype=jpg&er=1&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201312%2F05%2F20131205172342_P3nwv.jpeg";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    CircleImageView1 circleImageView21 = findViewById(R.id.image2_id1);
    circleImageView21.build(URL);


    }
    }

优点

能够实现对图片不同边框形状的支持。

缺点

实现较复杂,性能代价也比较高,且不支持设置外边框透明度。

使用BitmapShader重新绘制实现圆形图片

思路

和上一种方式比起来,第二种方式就显得简单很多了。主要思路就是利用bitmapShader将原图绘制出来,然后通过canvas.drawCircle绘制我们想要的边框即可。

代码

这里只贴出自定义viewCircleImageView2的代码,完整代码可以参考我后面给出的代码链接。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

public class CircleImageView2 extends ImageView {

public static final int DEFAULT_BORDER_WIDTH = 0;
private Paint mBorderPaint;
private Paint mPaint;
private int mRadius;
private Matrix mMatrix;
private int mBorderWidth;
private int mBorderColor;
private BitmapShader mBitmapShader;


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

public CircleImageView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public CircleImageView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(attrs);
}

private void initView(AttributeSet attrs) {
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircleImageView2);
mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView2_circle_border_width2, DEFAULT_BORDER_WIDTH);
mBorderColor = a.getInt(R.styleable.CircleImageView2_circle_border_color2, Color.BLACK);
a.recycle();
}
super.setScaleType(ScaleType.CENTER_CROP);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mMatrix = new Matrix();

if (mBorderWidth != DEFAULT_BORDER_WIDTH) {
mBorderPaint = new Paint();
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);

}

}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int min = Math.min(getMeasuredHeight(), getMeasuredWidth());
mRadius = min / 2;
setMeasuredDimension(min, min);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);

}

@Override
protected void onDraw(Canvas canvas) {
if (mBitmapShader == null) {
Bitmap bitmap = drawableToBitmap(getDrawable());
//CLAMP表示,当所画图形的尺寸大于Bitmap的尺寸的时候,会用Bitmap四边的颜色填充剩余空间。
//REPEAT表示,当我们绘制的图形尺寸大于Bitmap尺寸时,会用Bitmap重复平铺整个绘制的区域
//MIRROR与REPEAT类似,当绘制的图形尺寸大于Bitmap尺寸时,MIRROR也会用Bitmap重复平铺整个绘图区域,与REPEAT不同的是,两个相邻的Bitmap互为镜像。
if (bitmap == null) {
super.onDraw(canvas);
return;
}

mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//这里可以进行图片相关的处理,注释的代码是设置比例缩放
// float mScale = (mRadius * 2.0f) / Math.min(bitmap.getHeight(), bitmap.getWidth());
// mMatrix.setScale(mScale, mScale);
// mBitmapShader.setLocalMatrix(mMatrix);
}

mPaint.setShader(mBitmapShader);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
if (mBorderWidth != DEFAULT_BORDER_WIDTH) {
canvas.drawCircle(mRadius, mRadius, mRadius-mBorderWidth/2, mBorderPaint);
}
}

private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable == null)
return null;
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
//ARGB_8888 表示颜色色由4个8位组成即32位
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, width, height);
drawable.draw(canvas);
return bitmap;
}


public void build(String url) {
Glide.with(getContext())
.load(url)
.into(this);
}

}

与用这种方式实现没有边框的圆形图片相比,并添加复杂的逻辑,只是再次基础上画了一个圆,我们将画圆的画笔setStrokeWidth设置我们要实现的边框即可。

优点

实现比较简单,支持复杂的图片处理。

缺点

需要两次处理drawable,性能还是有一点的开销。

通过clipPath裁剪的方式

思路

我们在裁剪好的图片上画一个圆边框即可。与第二种方式实现圆类似。

代码

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
public class CircleImageView3 extends ImageView {

public static final int DEFAULT_BORDER_WIDTH = 0;
private float mRadius;
private int mBorderWidth;
private int mBorderColor;
private Paint mBorderPaint;
private Path mPath;

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

public CircleImageView3(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}

public CircleImageView3(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(attrs);
}

private void initView(@Nullable AttributeSet attrs) {
if (attrs!=null){
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircleImageView3);
mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView3_circle_border_width3, DEFAULT_BORDER_WIDTH);
mBorderColor = a.getInt(R.styleable.CircleImageView3_circle_border_color3, Color.BLACK);
a.recycle();
}
if (mBorderWidth != DEFAULT_BORDER_WIDTH){
mBorderPaint = new Paint();
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
}
mPath = new Path();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int min = Math.min(getMeasuredHeight(),getMaxWidth());
mRadius = min/2;
}

@Override
protected void onDraw(Canvas canvas) {
mPath.addCircle(mRadius, mRadius, mRadius,Path.Direction.CW);
canvas.clipPath(mPath);
super.onDraw(canvas);
canvas.drawCircle(mRadius, mRadius, mRadius-mBorderWidth/2, mBorderPaint);
}

public void build(String url) {
Glide.with(getContext())
.load(url)
.into(this);
}
}

优点

实现简单,不需要额外操作drawble,简单粗暴。

缺点

不支持复杂的图片处理。

源代码链接:https://github.com/Reoger/CurtomViewTest
上一篇链接:https://blog.csdn.net/Reoger/article/details/81394579

自定义View之将图片裁剪成圆形图片

使用PorterDuffXfermode方式来实现

在使用这中方式之前,我们有必要弄清楚PorterBuddx究竟是什么。
简单来说,他是定义的一种图像融合的方式,具体的模式有如下几种,效果如图:
官方的说明图

在次,不多做介绍,想深入了解PorterDuffXfermode可以参考各个击破搞明白PorterDuff.Mode
本次使用mode是SRCIN,由图效果可知,我们需要在原图上绘制一个圆,原图与我们绘制得圆重叠得区域就会显示出来,原理很简单,下面开始代码实现:

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 CircleImageView1 extends ImageView {

private Paint mPaint;
private Shape mShape;
private float mRadius;

private float[] outerRadii = new float[8];;

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

public CircleImageView1(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}

public CircleImageView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}

private void initView() {
setLayerType(LAYER_TYPE_HARDWARE,null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLACK);
//注意这里,将画笔的PorterDuffXfermode模式设置为了DST_IN模式!!!
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int min = Math.min(width,height);
mRadius = min/2;
setMeasuredDimension(min,min);
//因为是圆,所以要设置为长宽相等
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed){
if (mShape == null){
Arrays.fill(outerRadii,mRadius);
mShape = new RoundRectShape(outerRadii,null,null);
//
}
mShape.resize(getWidth(),getHeight());
}

}

@Override
protected void onDraw(Canvas canvas) {
int saveCount = canvas.getSaveCount();
canvas.save();
super.onDraw(canvas);
if (mShape != null){
mShape.draw(canvas,mPaint);
}
canvas.restoreToCount(saveCount);
}

// 这里直接使用Glide库来加载图片,也可以在xml文件或者java代码中设置图片
public void build(String url){
Glide.with(getContext()).load(url).into(this);
}
}

在布局中的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#333"
tools:context="com.example.cm.circleview.MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">

<com.example.cm.circleview.view.CircleImageView1
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:id="@+id/image_id1"
android:layout_width="200dp"
android:layout_height="200dp" />
</LinearLayout>

</ScrollView>

通过java代码设置图片来源,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {

public static final String URL = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533271076118&di=" +
"4ef2b0b50abb957177874a76d828126a&imgtype=0&src=http%3A%2F%2Fwww.91danji.com%2Fattachments%2F201503%2F24%2F11%2F3q8s74tc8.jpeg";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

CircleImageView1 circleImageView1 = findViewById(R.id.image_id1);
circleImageView1.build(URL);
}
}

因为项目使用到了Glide,所以需要在module的build.grade文件中添加必要的依赖,

1
implementation 'com.github.bumptech.glide:glide:3.8.0'

因为图片来自网路,需在androidMainFest中添加联网权限。
显示效果如图:
效果图

此次的实现都是最简单的实现,没有添加额外的逻辑,代码比较少,便于理解。
后续会在这个的基础上添加稍微复杂点的图像处理。

使用小结

通过该方式使得图片展现成圆形,需要开启硬件加速,否则图片的渲染效果达不到理想的效果,具体原因可以参照:硬件加速 setlayertype
我们可以通过shape或者drawable来绘制圆,使用然后通过图片的叠加显示来显示我们需要的原型图片的效果。这种方式就是通过绘制一个圆盖在原来的图片上,通过设置两张图片叠加显示效果来实现显示圆角图片

优点

该方式不需要操作原来的drawable,使用起来风险比较小,也比较方便。

缺点

需要启动硬件加速,否则可能会出现意想不到的结果。

使用BitmapShader重新绘制实现圆形图片

整体思路: 通过对原来图片进行处理,将原图片裁剪成圆形然后绘制展示。
贴上CircleImageView2的代码。

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
public class CircleImageView2 extends ImageView {

private Paint mPaint;
private int mRadius;
private float mScale;
private Matrix mMatrix;

private BitmapShader mBitmapShader;

public static final String TAG = "CircleImageView2";

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

public CircleImageView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public CircleImageView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(attrs);
}

private void initView(AttributeSet attrs) {

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mMatrix = new Matrix();
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int min = Math.min(getMeasuredHeight(), getMeasuredWidth());
mRadius = min / 2;
setMeasuredDimension(min, min);
//强制设置成宽度和高度一致
}

@Override
protected void onDraw(Canvas canvas) {
if (mBitmapShader == null) {
Bitmap bitmap = drawableToBitmap(getDrawable());
//CLAMP表示,当所画图形的尺寸大于Bitmap的尺寸的时候,会用Bitmap四边的颜色填充剩余空间。
//REPEAT表示,当我们绘制的图形尺寸大于Bitmap尺寸时,会用Bitmap重复平铺整个绘制的区域
//MIRROR与REPEAT类似,当绘制的图形尺寸大于Bitmap尺寸时,MIRROR也会用Bitmap重复平铺整个绘图区域,与REPEAT不同的是,两个相邻的Bitmap互为镜像。
mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//计算缩放比例
mScale = (mRadius * 2.0f) / Math.min(bitmap.getHeight(), bitmap.getWidth());
mMatrix.setScale(mScale, mScale);
}
//设置图片缩放
mBitmapShader.setLocalMatrix(mMatrix);
mPaint.setShader(mBitmapShader);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
}

// 将圆图片转化成bitmap
private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
//ARGB_8888 表示颜色色由4个8位组成即32位
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, width, height);
drawable.draw(canvas);
return bitmap;
}

public void build(String url) {
Glide.with(getContext())
.load(url)
.into(this);
}
}

实现逻辑:
首先将获得的drawable转化成bitmap,然后通过BitmapShader处理bitmap,并将其设置给画笔,当使用此画笔绘制时,该bitmap就会展示出来,如此我们只需要使用画布绘制一个圆形即可。

优点

可以实现对图片复杂的处理,可扩展性较强。

缺点

实现起来较为复杂,且有对

通过clipPath裁剪的方式

这种方式最简单暴力,直接在原来的图片上进行裁剪就ok了,如果不需要对图片进行处理的画,推荐用这种方式,因为,简单,粗暴!
直接上代码:

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 CircleImageView3 extends ImageView {

private float mRadius;
private Path mPath;

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

public CircleImageView3(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}

public CircleImageView3(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}

private void initView() {
mPath = new Path();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int min = Math.min(getMeasuredHeight(),getMaxWidth());
mRadius = min/2;
}

@Override
protected void onDraw(Canvas canvas) {
mPath.addCircle(mRadius, mRadius, mRadius,Path.Direction.CW);
canvas.clipPath(mPath);
super.onDraw(canvas);
}

public void build(String url) {
Glide.with(getContext())
.load(url)
.into(this);
}
}

没什么好说的,关键就一句canvas.clipPath(mPath);,需要关注一下path的用法。

优点

简单粗暴,不涉及drwable 相关的操作,效率较高,实现也简单。

缺点

因为是对图片的直接剪裁,不能实现对图片相关的处理,也没有抗锯齿效果,如果图片不是正方形图片可能会影响最终的效果。

源程序下载地址:https://github.com/Reoger/CurtomViewTest

总结

上述三种方式都能实现圆形图片的需求,但是各有优缺点。如果只是需要展示圆形图片而不需要对图片进行任何处理,可以选用第三种方式,需要对图片进行比较复杂的处理的画,则推荐第一种或者第二种方式。
本篇博客到此结束,下一篇将会使用上述的三种方式实现圆形外边框的效果。

参考链接

一键脚本搭建SS/搭建SSR服务并开启BBR加速

2018年1月20日 497条评论 127,696次阅读 293人点赞

自己写的一键搭建shadowsocks/搭建shadowsocksR的shell脚本,一键脚本适用Vultr上的和搬瓦工所有机型(CentOS、Ubuntu、Debian),搭建ss服务器支持所有客户端类型,本机你是iOS,Android,Windows,Mac,或者是Linux,搭建ss/ssr都是适用的科学上网方式。一键脚本搭建SS/SSR服务器,绝对没有任何问题,任何问题欢迎留言。一键脚本内容包括一键搭建shadowsocks/一键搭建shadowsocksR+一键开启bbr加速,适合新手小白。

纯新手也可以搭建ss/ssr,录了个视频教程,不想看文字的可以看视频,或者结合起来一起看:搭建ss视频教程

文章目录

什么是shadowsocks

shadowsocks可以指一种SOCKS5的加密传输协议,也可以指基于这种加密协议的各种数据传输包。

shadowsocks实现科学上网原理?shadowsocks正常工作需要服务器端和客户端两端合作实现,首先,客户端(本机)通过ss(shadowsocks)对正常的访问请求进行SOCK5加密,将加密后的访问请求传输给ss服务器端,服务器端接收到客户端的加密请求后,解密得到原始的访问请求,根据请求内容访问指定的网站(例如Google,YouTube,Facebook,instagram等),得到网站的返回结果后,再利用SOCKS5加密并返回给客户端,客户端通过ss解密后得到正常的访问结果,于是就可以实现你直接访问该网站的“假象”。

为什么选择shadowsocks?不限终端(安卓,苹果,Windows,Mac都可用),流量便宜(服务器500G只要15元),方便(一键脚本,不需要专业知识)。

为什么要自己搭建ss/ssr?你也许会觉得买ss服务也很方便,但是你得要考虑以下几个问题。首先,买的ss服务,限制很多,终端可能只能同时在线2个,每个月就一点点流量可能价格却不便宜,有时候还被别人做手脚,流量跑的贼快;其次,别人收钱跑路怎么办?很多这种情况的;更重要的是,如第一个问题中描述的shadowsocks原理,如果有心人做了一点手脚,是可以得到你的访问记录的;而自己搭建ss/ssr服务,一键脚本也就10来分钟就可以搞定。

一键脚本搭建ss/ssr支持系统版本

脚本系统支持:CentOS 6+,Debian 7+,Ubuntu 12+

注:这个脚本支持的系统版本是指ss服务器的版本(都没看过也没关系,不影响搭建),你本机是Windows、Mac、Linux,或者你想用手机端搭建ss/ssr服务器,安卓和苹果,都是可以的。

代理服务器购买

作为跳板的代理服务器推荐Vultr搬瓦工,一是因为本脚本在这两家的所有VPS都做了测试,二是因为都是老牌VPS服务商,不怕跑路。

推荐你们使用Vultr:优惠注册链接,购买图解与节点推荐可以参考Vultr购买图解步骤,最低月付2.5刀,也是目前博主自用以及运行小站的VPS,月付方便,随时重置。

对于宽带是电信或者是联通的用户,可以试一下搬瓦工的CN2电信/联通直连线路(季付/半年付/年付),GT线路详情可以参考搬瓦工洛杉矶CN2 GT线路测评。如果想体验最优的GIA线路,也可以尝试搬瓦工CN2 GIA线路(强力推荐,效果爆炸,全程CN2,年付平均下来一个月30元左右,晚上高峰时期线路也不拥堵。2018年5月15日正式启动,搬瓦工洛杉矶CN2 GIA线路测评),当然你也可以直接用Vultr,搬瓦工暂时不支持月付。

Vultr和搬瓦工上的所有机型是绝对可以一键脚本搭建shadowsocks/搭建shadowsocksR+开启bbr加速成功的,任何问题欢迎留言~

连接远程Linux服务器

购买完成后根据Windows通过Xshell连接Linux或者Mac通过Terminal远程连接Linux即可。

你如果身边没有电脑,一定要搞什么手机搭建ss服务器 :symbols: 也是可以的,毕竟一键脚本只需要复制几行脚本命令就行了。iOS用户可以使用Termius这个工具,直接在App Store下载就行。Android没有用过,反正能ssh连接的软件就行~

一键搭建SS/搭建SSR服务

注意,shadowsocks/shadowsocksR这两个只需要搭建一个就可以了!!!!SS与SSR之间的比较一直是各有各的说法,王婆卖瓜自卖自夸。我用的是SS,因为SS的iOS版本比较容易下载,并且被没有觉得ss容易被探查到~

一键搭建shadowsocks

购买VPS并用Xshell连接上你刚购买的VPS后,你将看到如下图所示的界面:

vutlr-connect-result

如红框中所示,root@vult(root@ubuntu)说明已经连接成功了,之后你只需要在绿色光标处直接复制以下代码就可以了(直接复制即可,如每段代码下方截图中所示)。

1.下载一键搭建ss脚本文件(直接在绿色光标处复制该行命令回车即可,只需要执行一次,卸载ss后也不需要重新下载)

git clone https://github.com/flyzy2005/ss-fly

shadowsocks-ss-fly-clone

如果提示bash: git: command not found,则先安装git:

1
2
Centos执行这个: yum -y install git
Ubuntu/Debian执行这个: apt-get -y install git

2.运行搭建ss脚本代码

1
ss-fly/ss-fly.sh -i flyzy2005.com 1024

其中flyzy2005.com换成你要设置的shadowsocks的密码即可(这个flyzy2005.com就是你ss的密码了,是需要填在客户端的密码那一栏的),密码随便设置,最好只包含字母+数字,一些特殊字符可能会导致冲突。而第二个参数1024端口号,也可以不加,不加默认是1024~(举个例子,脚本命令可以是ss-fly/ss-fly.sh -i qwerasd,也可以是ss-fly/ss-fly.sh -i qwerasd 8585,后者指定了服务器端口为8585,前者则是默认的端口号1024,两个命令设置的ss密码都是qwerasd):

shadowsocks-install

界面如下就表示一键搭建ss成功了:

ss-fly-success-new

注:如果需要改密码或者改端口,只需要重新再执行一次搭建ss脚本代码就可以了,或者修改/etc/shadowsocks.json这个配置文件。

3.相关ss操作

1
2
3
4
5
6
7
8

修改配置文件:vim /etc/shadowsocks.json

停止ss服务:ssserver -c /etc/shadowsocks.json -d stop

启动ss服务:ssserver -c /etc/shadowsocks.json -d start

重启ss服务:ssserver -c /etc/shadowsocks.json -d restart

4.卸载ss服务

1
ss-fly/ss-fly.sh -uninstall

一键搭建shadowsocksR

再次提醒,如果安装了SS,就不需要再安装SSR了,如果要改装SSR,请按照上一部分内容的教程先卸载SS!!!

1.下载一键搭建ssr脚本(只需要执行一次,卸载ssr后也不需要重新执行)

git clone https://github.com/flyzy2005/ss-fly,此步骤与一键搭建ss一致,可以参考:下载脚本

2.运行搭建ssr脚本代码

1
ss-fly/ss-fly.sh -ssr

ss-fly-insall-ssr

3.输入对应的参数

执行完上述的脚本代码后,会进入到输入参数的界面,包括服务器端口,密码,加密方式,协议,混淆。可以直接输入回车选择默认值,也可以输入相应的值选择对应的选项:

ss-fly-ssr-options

全部选择结束后,会看到如下界面,就说明搭建ssr成功了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Congratulations, ShadowsocksR server install completed!

Your Server IP        :你的服务器ip

Your Server Port      :你的端口

Your Password :你的密码

Your Protocol :你的协议

Your obfs :你的混淆

Your Encryption Method:your_encryption_method

Welcome to visit:https://shadowsocks.be/9.html

Enjoy it!

4.相关操作ssr命令

1
2
3
4
5
6
7
8
9
10
11
12
13
启动:/etc/init.d/shadowsocks start

停止:/etc/init.d/shadowsocks stop

重启:/etc/init.d/shadowsocks restart

状态:/etc/init.d/shadowsocks status

配置文件路径:/etc/shadowsocks.json

日志文件路径:/var/log/shadowsocks.log

代码安装目录:/usr/local/shadowsocks

5.卸载ssr服务

1
./shadowsocksR.sh uninstall

一键开启BBR加速

BBR是Google开源的一套内核加速算法,可以让你搭建的shadowsocks/shadowsocksR速度上一个台阶,本一键搭建ss/ssr脚本支持一键升级最新版本的内核并开启BBR加速。

BBR支持4.9以上的,如果低于这个版本则会自动下载最新内容版本的内核后开启BBR加速并重启,如果高于4.9以上则自动开启BBR加速,执行如下脚本命令即可自动开启BBR加速:

1
ss-fly/ss-fly.sh -bbr

ss-fly-bbr-success-new

装完后需要重启系统,输入y即可立即重启,或者之后输入reboot命令重启。

判断BBR加速有没有开启成功。输入以下命令:

1
sysctl net.ipv4.tcp_available_congestion_control

如果返回值为:

1
net.ipv4.tcp_available_congestion_control = bbr cubic reno

后面有bbr,则说明已经开启成功了。

客户端搭建shadowsocks/shadowsockR代理实现科学上网

客户端搭建ss代理

各种客户端版本下载地址:各版本shadowsocks客户端下载地址

以Windows为例(shadowsocks电脑版(windows)客户端配置教程):

shadowsocks-pc-windows

在状态栏右击shadowsocks,勾选开机启动启动系统代理,在系统代理模式中选择PAC模式服务器->编辑服务器,一键安装shadowsocks的脚本默认服务器端口是1024,加密方式是aes-256-cfb,密码是你设置的密码,ip是你自己的VPS ip,保存即可~

PAC模式是指国内可以访问的站点直接访问,不能直接访问的再走shadowsocks代理~

OK!一键脚本搭建shadowsocks完毕!科学上网吧,兄弟!Google

客户端搭建ssr代理

各种客户端版本下载地址:各版本SS客户端&SSR客户端官方下载地址

以Windows为例:

ssr-pc-windows-config

在状态栏右击shadowsocksR,在系统代理模式中选择PAC模式,再左击两次状态栏的图标打开编辑服务器界面,如上图所示,按照自己的服务器配置填充内容保存即可~

PAC模式是指国内可以访问的站点直接访问,不能直接访问的再走shadowsocksR代理~

OK!一键脚本搭建shadowsocksR完毕!科学上网吧,兄弟!Google

vultr搭建ss视频教程

应读者要求录了个视频教程,如果你觉得这些文字还不够生动,不够清楚的话,可以看一下视频教程。

视频获取方式:关注微信公众号flyzy小站,发送视频即可获得。

flyzy小站

一键脚本更新日志

一键脚本源码:一键搭建shadowoscks/搭建shadowsocksR并开启bbr内核加速

2018年1月20日,上传一键安装shadowsocks脚本

2018年1月24日,添加升级内核并开启BBR加速功能

2018年3月28日,将升级内核&&开启BBR加速集成在一个命令中

2018年4月4日,添加一键安装shadowsocksR功能(调用的teddysun大大的一键搭建SSR脚本 :eek:

2018年4月27日,支持Ubuntu/CentOS/Debian

2018年5月29日,支持一键安装V2Ray

关注公众号flyzy小站,上面有一些搭建shadowsocks常见问题的总结如果还是不行,欢迎在公众号留言

声明:本文只作为技术分享,请遵守相关法律,严禁做违法乱纪的事情!

Telegram频道已经开通,关注flyzythink,随手分享正能量,了解VPS优惠与补货
Telegram群组已经开通,加入flyzy小站,FREE TO TALK
QQ群开通:780593286 flyzy小站

原创声明

该日志由 flyzy小站 于2018年01月20日发表在shadowsocks分类下, 你可以发表评论,并在保留原文地址及作者的情况下转载到你的网站或博客。
转载请注明: 一键脚本搭建SS/搭建SSR服务并开启BBR加速 | flyzy小站
关键字: bbr一键, shadowsock, ss/ssr脚本, 一键搭建ss脚本, 一键脚本搭建ssr, 科学上网

weinxin
我的微信公众号

“flyzy小站”:关注VPS测评与优惠,玩机教程,技术分享,留言及时回复

打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮

bbr一键 shadowsock ss/ssr脚本 一键搭建ss脚本 一键脚本搭建ssr 科学上网

< 上一篇

下一篇 >

相关文章

热门标签

搬瓦工 Linux shadowsock配置 多用户 Java编程思想读书笔记 百度联盟 VPS iptables JVM shadowsock 工具 SEO WordPress vultr DigitalOcean ss 科学上网 建站 数据结构 CN2 vps推荐

VPS推荐

| 搬瓦工优惠券优惠码
搬瓦工:年付最低仅需18.79美元 |
| vultr优惠码
Vultr:15个数据中心,最低2.5美元/月 |
| gigsgigscloud优惠码
GigsGigsCloud:香港VPS,三网直连 |

更多

window简介

Window 有三种类型,分别是应用 Window、子 Window 和系统 Window。应用类 Window 对应一个 Acitivity,子 Window 不能单独存在,需要依附在特定的父 Window 中,比如常见的一些 Dialog 就是一个子 Window。系统 Window是需要声明权限才能创建的 Window,比如 Toast 和系统状态栏都是系统 Window。

Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖在层级小的 Window 上面,这和 HTML 中的 z-index 概念是完全一致的。在三种 Window 中,应用 Window 层级范围是 199,子 Window 层级范围是 10001999,系统 Window 层级范围是 20002999,我们可以用一个表格来直观的表示:
|Window|层级|
|—|—-|
|应用 Window|1
99|
|子 Window|10001999|
|应用 Window|2000
2999|
这些层级范围对应着 WindowManager.LayoutParams 的 type 参数,如果想要 Window 位于所有 Window 的最顶层,那么采用较大的层级即可,很显然系统 Window 的层级是最大的,当我们采用系统层级时,需要声明权限。

悬浮窗简单使用

悬浮窗通过WindowManager来实现,它提供的三个重要的方法为:

1
2
3
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);

我们只需要将需要显示的view添加到windowManager中即可。但是因为是系统层的显示,在addView时需要必要的权限

1
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

并且在android 6.0及以上的版本时,还需要申请动态权限,这里就不展开了。

悬浮窗工具类

通过一个工具类实现通过悬浮窗实现类似于toast的效果。代码如下:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public class FloatWindowManager {

private WindowManager.LayoutParams mParams;
private WindowManager mWindowManager;

private boolean mIsRunning = false;
private boolean mIsAutoDisMiss = true;

private TimerTask mTask;
private Timer mTimer;

public final static int DEFAULT_FINISH_TIME = 2000;
private int mAutoDismissTime = DEFAULT_FINISH_TIME;

public FloatWindowManager() {
mWindowManager = (WindowManager) HostHelper.getAppContext().getSystemService(Context.WINDOW_SERVICE);
mParams = new WindowManager.LayoutParams();
}

public FloatWindowManager initParams() {
if (mParams != null) {
if (Build.VERSION.SDK_INT >= 26) {
mParams.type = WindowManager.LayoutParams.TYPE_PHONE;
} else {
mParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
mParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SCALED
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
mParams.format = PixelFormat.RGBA_8888;
}
return this;
}


public FloatWindowManager setLocation(int x,int y,int width,int height){
if (mParams!=null){
mParams.x = x;
mParams.y = y;
mParams.height = height;
mParams.width = width;
}
return this;
}

public FloatWindowManager setGravity(int gravity){
if (mParams!=null){
mParams.gravity = gravity;
}
return this;
}

public FloatWindowManager setAnim(int anim){
if (mParams!=null){
mParams.windowAnimations = anim;
}
return this;
}

public FloatWindowManager setAutoDismiss(boolean autoDismiss){
this.mIsAutoDisMiss = autoDismiss;
return this;
}


public boolean show(View view) {
if(CloudConfigUtils.getToastActivityShow()){
try {
if (mWindowManager != null && mParams!=null && !mIsRunning) {
mWindowManager.addView(view, mParams);
if (mIsAutoDisMiss){
finishByTime(view);
}
Log.d("FloatWindowManager"," view is added!");
mIsRunning = true;
return true;
}

} catch (Exception e) {
mIsRunning = false;
Log.d("FloatWindowManager", "addView go something wrong!");
return false;
}
}
return false;
}

private void finishByTime(final View view){
mTimer = new Timer();
mTask = new TimerTask() {
@Override
public void run() {
finisSelf(view);
}
};
mTimer.schedule(mTask, mAutoDismissTime);
}

private void finisSelf(final View view){
try {
if (mWindowManager !=null && mParams!=null && mIsRunning){
mWindowManager.removeView(view);
mIsRunning = false;
if (mTask!=null)
mTask = null;
if (mTimer!=null)
mTimer = null;
}
Log.d("FloatWindowManager"," view is remove!");
} catch (Exception e) {
mIsRunning = false;
if (mTask!=null)
mTask = null;
if (mTimer!=null)
mTimer = null;
Log.d("FloatWindowManager", "remove go something wrong!");
}
}

}

调用也很简单,只需要一行代码:

1
2
3
4
5
6
7
8
9
//自定义view
final AccountToastLayout layout = new AccountToastLayout(mContext);
//通知测量自定义view的宽和高
layout.measure(View.MeasureSpec.UNSPECIFIED,View.MeasureSpec.UNSPECIFIED);
int weight = layout.getMeasuredWidth();
final int height = layout.getMeasuredHeight();
FloatWindowManager windowManager = new FloatWindowManager();
windowManager.initParams().setAutoDismiss(true).setAnim(R.style.anim).setGravity(Gravity.BOTTOM|Gravity.END)
.setLocation(0,y,weight,height).show(layout);

windowManager中相关属性的介绍

type字段介绍

value key 作用 备注
2031 TYPE_ACCESSIBILITY_OVERLAY 用于拦截用户交互而不更改窗口,可访问性服务可以内省。 added in API level 22
2 TYPE_APPLICATION 正常的应用程序窗口。token必须是标识activity所属window的标记。 added in API level 1
1003 TYPE_APPLICATION_ATTACHED_DIALOG 类似于TYPE_APPLICATION_PANEL,但窗口的布局与顶层窗口的布局相同,而不是其容器的子窗口。 added in API level 3
1001 TYPE_APPLICATION_MEDIA 用于显示媒体的窗口(例如视频)。这些窗口显示在其附加窗口后面。 added in API level 3

….

flag字段介绍

看这里https://developer.android.com/reference/android/view/WindowManager.LayoutParams

参考链接