文章分类 » Android

超星雅儿跳课Xposed模块开发

目的实现:超星雅儿跳课Xposed模块开发

前言

上大学的时候大多数的本科或者专科学生会在 超星XX选一门课程作为通修课,然后下载app看视频。最恶心的事情是什么?不能拖动进度条和快进 除非你看完了再看第二边。 而我们利用HOOK技术实现快速看完视频。。。

以下内容仅供学习,禁止商业用途。。。
适用超星版本:3.0.2(现在最新的了吧)
以下教程因为涉及大多高校正常教学制度。所以教程加密并且不公开。核心教程和源码地址(已经加密):

我的思路一: 先不管 三七二十一 反编译app 很可惜 加固了。。。脱壳拖了几天最后把自己裤子了睡觉

思路二:抓取数据包 然后分析访问接口的规则 然后自己把自己本地架设成远程访问服务器。

思路三:HOOK

思路一走不通,对于思路二我在想 核心接口都用了某个加密规则加密了吧?因为高考数学没到140所以放弃吧。我最后考虑最后一个。

提示:我原本思路二是解析加密规则麻烦,返现app某个接口没有加密,后来英语四级没时间咯,所以只实现HOOK。。
有人问我 这一步有人问我怎么做。这里提供一个小思路 ,我们电脑访问www.baidu.com ,先会到电脑C:\Windows\System32\drivers\etc\hosts 寻找域名和端口的射影,如果不存在 会像网络的DNS(域名系统)寻找。假设app 访问 www.XXXX.com 你改下射影文件不就好了?(别问我这不是windos下的修改方法,稍微变通下你懂的)然后你反回一段完成此视频的信息,那么你就可以拖动进度条了。

HOOK初探:
这里是基于XPosed编写的。
我的HOOK思路之一:首先查看视频播放界面。DDMS有dump view工具。
然后看到如下界面:
这里写图片描述

看到一个SeekBar,我就想着开发者是不是给SeekBar添加一个监听,判断是不是用户手动拖动的(监听回调有一个参数判断是不是手动滑动的进度条)。
1 如果是用户手动拖动的那么判断是否看过视频,如果没看过,禁止拖动
2 如果不是那么不管

结论:失败。。。。

tip:布局中还有个Seekbar那时候我获取错了,弄了半天

直接反射修改SeekBar 进度。

结论:失败

tip:得到思路 作者应该每次拖动的时候判断 你以上看到哪了,或者是否看完了。

所以。。。。。。 以下思路加密提供

思路和源码(已加密)

Xposed模块开发教程

(译)Xposed模块开发教程

原文地址。这是开发者所写的,可以说是官方开发指南。文章讲述了Xposed的原理,以及怎么开发Xposed框架的模块。头一次翻译技术文档,有错误的话请多包涵。

好了,你想学习怎么为Xposed开发新的模块么?那么读读这篇教程(或者我们可以称他为”泛读短文”)学着怎么去做。这不仅包括“创建这个文件然后插入…”这类的技巧,也包括这些技巧背后的思想。这些思想正是创造价值的步骤以及你真正需要了解你做了什么和为什么这么做的原因。如果你觉得本文“太长,不想读”,那么你可以只看最后的源代码和阅读“使工程成为Xposed模块“部分。但是如果你读了整篇文章你就会有更好的理解。你之后会节省出来阅读这个的时间,因为你不必凭自己弄清楚每件事。

修改主题


你将重新创建在github上可以找到的红色钟表的的例子。它包括将状态栏的钟表变为红色并且加入一个笑脸的功能。我选择这个例子是因为它非常小,而且容易看见所做的修改。并且,它也使用了框架所提供的一些基本方法。

Xposed如何工作


在你开始做出自己的修改之前,你应当大致了解Xposed如何工作(如果觉得这部分无聊可以跳过)。以下就是原理:

有一个叫做”Zygote”的进程,它是android运行环境的核心。每个应用都从一份它的拷贝(“fork”)产生。这个进程在手机启动时由一个叫 /init.rc 的脚本启动。这个进程的启动在 /system/bin/app_process 加载所需要的类和调用初始化方法后完成。

这里就是Xposed发挥用处的地方了。当你安装完框架后,一个扩展过的app_process就会被复制到 /system/bin 下。这个扩展过的启动进程会将一个额外的jar包添加到环境变量,并在特定场合调用里面的方法。比如:当虚拟机创建完成后和Zygote的main方法被调用前。并且在那个方法当中,我们已经是Zygote的一部分,而且能够在它的上下文context中活动。

jar包的位置是 /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar 它的源代码可以在这里找到。查看XposedBridge的类,你能找到main方法。这就是我上文中所写过的,它在每个进程的最开始部分被调用。一些初始化的工作在那里完成,并且我们的模块在那里加载(之后我再讲模块的加载)。

方法的hook/替换


真正使Xpoesed有威力的就是hook方法调用。当你反编译并修改APK时,你能够在任何你想的地方直接修改/替换指令。然而,你事后需要重新编译/给APK签名,并且只能发布整个安装包。使用Xposed能让你放置的hook,你并不能修改程序内部的方法代码(清楚地定义你想要在何处做什么样的修改是不可能的)。然而,你可以在方法调用的前后注入你的代码。这也是java中能够被清楚寻址的最小单位。

XposedBridge 有一个私有的 native 方法叫做 hookMethodNative。这个方法也在扩展后的 app_process 中被实现了。它会将方法类型转为“native”,并把方法的实现与本地的通用方法相连。这意味着,每当被hook的方法调用后,调用者不知道实际调用的是通用的方法。在这个方法中,位于 XposedBridge 的 handleHookedMethod 方法会被调用,并向方法调用传递参数、this指针以及其他东西。之后这个方法负责唤起之前方法调用注册过的回调。上述这些行为能够改变调用的参数、实例/静态变量、唤起其他方法、处理调用结果。。。或者跳过这些东西。它的弹性非常大。

好了,理论讲够了。我们现在创建一个模块吧!

创建工程


一个模块就是一个普通的app,只不过多了一些特殊的文件和元数据。所以在我们创建新的android工程以前,我假设你已经做过这个了。如果没有,官方文档讲的很详细。对于SDK,我选择了4.0.3(API15)。我建议你也使用这个,并且不要立刻开始。你不需要创建Activity,因为我们的修改不需要任何用户界面。回答过了这个问题后,你应该有一个空白的工程项目。

使工程成为Xposed模块


现在我们把工程变成Xposed能加载的东西。我们需要以下几个步骤。

AndroidManifest.xml

Xposed Installer的模块列表搜寻所有有一种特殊元数据标记的应用程序。你可以到 AndroidManifest.xml => Application => Application Nodes (在底部) => Add => Meta Data 下面去创建这个标记。标记名称应该是 xposedmodule ,值应该是 true。给resource留空。重复以上过程创建 xposedminversion (见下文) 和 xposeddescription (你创建的模块的简单描述)。XML文件现在就是这个样子:

<?xml version="1.0" encoding="utf-8"?>
 <manifest  xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.robv.android.xposed.mods.tutorial"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Easy example which makes the status bar clock red and adds a smiley" />
        <meta-data
            android:name="xposedminversion"
            android:value="30" />
    </application>
</manifest>

XposedBridgeApi.jar

接下来,让程序能够找到 XposedBridge 的API。你可以从
这里下载 XposedBridgeApi-<version>.jar 的最新版。把它复制到叫做lib的子文件夹下。右键单击选择Build Path => Add to Build Path。文件名当中的<version>是你在manifest文件的xposedminversion标签所插入的版本。

保证API类没有被包含(但仅仅是参考)在你编译过的APK里,否则你会得到一个IllegalAccessError错误。libs(含有s)文件夹是eclipse自动生成的,不要把API文件放在那里。

模块的实现

现在你可以给你的模块创建一个类了。我的类叫做”Tutorial”,位于de.robv.android.xposed.mods.tutorial这个包中。

package de.robv.android.xposed.mods.tutorial;

public class Tutorial {

}

第一步,我们仅仅生成一些日志表明模块已经加载。一个模块可以有多个入口点。你选择哪个取决于你想修改什么。你可以在安卓系统启动时、在一个app将要启动时、在一个app的资源文件初始化时或其他时候,调用一个函数。

在这个教程靠后面的一部分,你将了解到在一个特定的app中需要做出的修改。那么先让我们了解一下 “让我知道什么时候加载一个新app” 这个入口点。所有入口点都被标记为IXposedMod的子接口。这种情况下,你需要实现 IXposedHookLoadPackage 这个接口。其实它只有一个仅有一个参数的方法。这个方法向被实现的模块提供更多关于运行环境上下文的信息。在我们的例子中,我们用log输出加载的app的名称。

package de.robv.android.xposed.mods.tutorial;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        XposedBridge.log("Loaded app: " + lpparam.packageName);
    }
}

这个log方法向标准logcat以及 /data/data/de.robv.android.xposed.installer/log/debug.log(通过Xposed Installer可以轻易访问到)输出信息(tag Xposed)。

assets/xposed_init

现在唯一遗漏的就是提示XposedBridge哪些类包含了入口点。这项工作通过一个叫 xposed_init 的文件完成。在assets文件夹下创建一个新的名叫xposed_init的text文件。在该文件中每行包含一个类的全名。在这个例子中,它是 de.robv.android.xposed.mods.tutorial.Tutorial。

试试看


保存你的文件。以Android Application的方式运行你的程序。因为这是你第一次安装它,在使用前你需要先启用。打开Xposed Installer这个app并确保你安装了xposed框架。之后切换到Modules标签。你应该能在那里找到你的app。在选择框内打钩使得它可用。然后重启。你当然什么变化也看不到,但如果检查log记录,以应该会看见以下的东西:

Loading Xposed (for Zygote)...
Loading modules from   /data/app/de.robv.android.xposed.mods.tutorial-1.apk
Loading class de.robv.android.xposed.mods.tutorial.Tutorial
Loaded app: com.android.systemui
Loaded app: com.android.settings
... (many more apps follow)

瞧!它起作用了。现在你拥有了一个Xposed模块。它能够变得比写一些log更加有用…

探索你的目标并寻找修改它的方式


好了,下面要开始讲的部分也许会非常不同,这取决于你想做什么。如果你之前修改过apk,也许你会知道在这里应当如何思考。总的来说,你需要了解目标的一些实现细节。在本教程中,目标选定为状态栏的时钟。这有助于了解到状态栏以及其他一些东西都是系统UI的一部分。现在让我们在那里开始我们的探索。

可能性1:反汇编。这会告诉你它实际的实现,但是会很难阅读和理解,因为你得到的都是smali格式的东西。可能性2:获得AOSP源代码。比如这里这里。ROM不同代码也很不一样,但在本例中他们的实现是相似的甚至是相同的。我会先看AOSP,然后看看这么做够不够。如果我需要细节,我会看看实际的反汇编的代码。

你可以找找名称中有“clock”的类。其他需要找的是用到的资源和布局。如果你下载官方的AOSP代码,你可以从 frameworks/base/packages/SystemUI 开始找。你会找到好几处“clock”出现的地方。找到有好几种方式实现修改是正常而且真实的。时刻记住你“只能” hook方法。所以你必须另找其他能够插入代码实现功能的地方,要么在方法的前面或是后面,或者是替换掉方法。你应当hook一个尽可能明确的方法,而不是一个被调用成千上万次的用于解决性能问题和非计划中的副作用的方法。

在本例当中,你或许会发现布局 res/layout/status_bar.xml 包含一个指向带有类com.android.systemui.statusbar.policy.Clock的自定义view。现在你脑子里也许会有好多点子。文字的颜色是通过textAppearance属性定义的,所以最干净的更改它的方法就是修改外观的定义。然而,用Xposed框架改变外观属性几乎是不可能的(这需要深入本地代码)。替换状态栏的布局也许是可能的,但对于你试图做出的小小修改来说是杀鸡用牛刀。取而代之的是,看看这个类。有一个叫updateLock的方法,似乎每分钟都调用一次用于更新时间。

final void updateClock() {
    mCalendar.setTimeInMillis(System.currentTimeMillis());
    setText(getSmallTime());
}

这个方法用于修改来说是很好的,因为这是一个足够具体的看似唯一能够修改时钟文字的方法。如果我们在这个方法的每次调用之后都加些修改时钟文字和颜色的东西,应该就能起作用。那么,我们开始做吧。

对于单独修改字体颜色部分,有一种更好的办法。参见“替换资源”中“修改布局”的例子。

使用反射寻找并hook方法


现在我们已经知道了哪些东西?我们在com.android.systemui.statusbar.policy.Clock有一个叫做updateClock的希望干涉的方法。我们在系统UI资源中找到了这个类,所以它仅仅在系统UI进程当中有效。其它一些类属于框架,而且在任何地方都有效。如果我们在 handleLoadPackage 中试图直接获取任何这个类的信息和引用,就会失败。因为处于错误的进程中。所以让我们实现一种仅在某个特定包即将加载时执行特定代码的情况:

public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
    if (!lpparam.packageName.equals("com.android.systemui"))
        return;

    XposedBridge.log("we are in SystemUI!");
}

使用参数,我们能轻松检查是否在正确的包中。一旦我们核实了这一点,我们就通过ClassLoader取得那个包中的也被这个变量引用的类。现在我们可以寻找com.android.systemui.statusbar.policy.Clock这个类以及它的updateClock方法,然后告诉XposedBridge去hook这个方法:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called before the clock was updated by the original method
            }
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called after the clock was updated by the original method
            }
    });
    }
}

findAndHookMethod是一个Helper函数。注意静态导入,如果你像连接中说的一样做了,那就会被自动加入。这个方法使用系统UI包的ClassLoader寻找 Clock 这个类,然后寻找其中的updateClock方法。如果这个方法中有任何参数,你必须事后列出这些参数的类型。有许多方法做这件事,但因为我们的方法没有参数,所以就先跳过这个步骤。至于最后一点,你需要提供一个 XC_MethodHook 类的实现。对于更小的更改,你可以使用匿名类。如果你的代码很多,最好创建普通的类并且只在这里创建实例。helper之后会如之前所说的,做出所有hook这个方法的必要工作。

XC_MethodHook中有两个你可以重写的方法。你可以两个都重写,或者都不重写。但后者的做法显然是没有道理的。这些方法就是beforeHookedMethod 和 afterHookedMethod。不难猜出他们在原有方法之前/后执行。你可以使用“before”方法在方法调用前估计/操纵参数(通过param.args)。甚至阻止调用原来的方法(发送你自己的结果)。“after”方法可以用来做一些基于原来方法的结果的事。你也可以在这个地方操纵结果。当然,你可以在方法调用的前/后添加你自己要执行的代码。

如果你要彻底替换一个方法,去看看子类XC_MethodReplacement。在那里,你只需要重写replaceHookedMethod。

XposedBridge为每个hook过的方法维护一个注册过的回调的表。拥有最高优先级(在hookMethod中被定义)的将被优先调用。原有的方法总是具有最低的优先级。所以如果你hook了一个拥有回调A(高优先级)和回调B(默认优先级)的方法,那么不管被hook的方法是如何被调用的,执行顺序总是这样的:A.before -> B.before -> original method -> B.after -> A.after。所以A可以影响B看到的参数,即把它们传递下去以前大幅度地修改它们。原有的方法的结果会先被B处理,但是A拥有原先调用者最终将得到什么样结果的决定权。

最终步骤:在方法调用之前/后执行你自己的代码

好了,你已经在正确的上下文运行环境中(比如:系统UI进程)有了一个在updateClock方法每次被调用时都会被调用的方法。现在让我们修改一些东西吧。

第一个要检查的:我们有具体的Clock类型的引用么?是的,我们有。这就是param.thisObject参数。所以如果方法通过myClock.updateClock()被调用,那么param.thisObject 就是 myClock。

接下来:我们对clock做什么?Clock类型并不可用,你不能将param.thisObject转换为类(也不要试着这样做)。然而它是从TextView继承而来。所以一旦你把Clock的引用转换为TextView,你可以使用像setText, getText 和 setTextColor之类的方法。我们的改动应该在原有的方法设置了新的时间以后进行。因为在方法被调用前什么都没有做,我们可以空着beforeHookedMethod。没有必要调用(空的)“super”方法。

所以以下是完整的源代码:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                TextView tv = (TextView) param.thisObject;
                String text = tv.getText().toString();
                tv.setText(text + " :)");
                tv.setTextColor(Color.RED);
            }
        });
    }
}

对结果感到满意


现在启动/安装你的app。因为你第一次启动它时已经在Xposed Installer中把它设置为了可用,你就不需要在做这一步了。重启即可。然而,如果你正在使用这个红色钟表的例子,你也许想禁用它。两者都对它们的updateClock 处理程序使用了默认的优先级。所以你不清楚哪个会胜出(实际上这依赖于处理方法的字符串表示形式,但不要依赖这个方式)。

结论


我知道这个教程很长。但我希望你现在不但能实现一个绿色的表,也能够做出一些完全不同的东西。找到好的被hook的方法是经验的问题,所以先从简单的做起。试着一开始多使用log函数确保每次调用都是你预期的。现在:祝玩得开心!

Android之免清单注册启动Activity

在此立志:我要努力大学毕业进BAT


实习目标:Activity不需要注册在清单即可通过intent启动。

  • 有些文章叫做hook技术。大致内容为监听方法或者的调用或触发,期间修改方法参数或者返回值达到无须需改app源码即可修改app。如Xpose有插件可防止qq撤销消息。

  • 我们今天监听activity的启动然后进行方法修改,期间会用动态代理和大量的反射

Activity启动流程分析 第一章

首先学习activity启动流程:
假设我们界面有个按钮 点击后触发activityonclick方法

//activity
public void onClick(View view) {
        Intent intent = new Intent(this,SecondActivity.class);
        startActivity(intent);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

按照我们的理解接下来的事情自然就是界面跳转了。我主要研究的startActivity内部实现的调用过程到启动一个activity流程。
这个方法的实现是在ContextImpl.java 中然后我们再看继承关系图:
这里写图片描述
结果方法继承关系里面并没有ContextImpl.java 。他其实被包裹在ContextWrapper.java

//ContextWrapper.java
public class ContextWrapper extends Context {
    Context mBase;
    ...省略其他代码
 @Override
    public void startActivity(Intent intent) {
        mBase.startActivity(intent);
    }
    ...省略其他代码  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到里面有个mBase属性对象。在调用startActivity的时候会调用mBasestartActivity方法。这个mBase就是ContextImpl.java

现在来分析ContextImpl.java
我们看看这个类的startActivity的方法的实现

//ContextImpl.java
    @Override
    public void startActivity(Intent intent) {
        warnIfCallingFromSystemProcess();
        startActivity(intent, null);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

直接看startActivity(intent, null);方法,前一个方法是用于进程检测的。

 @Override
    public void startActivity(Intent intent, Bundle options) {
      //...省略代码 只看方法的重点
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, options);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可以看到此方法调用 mMainThread.getInstrumentation()获取一个对象然后调用对象的方法啊execStartActivity。其中 mMainThread对象的声明为final ActivityThread mMainThread;

mMainThread.getInstrumentation()查看实现如下

//ActivtyThread.java
 public Instrumentation getInstrumentation()
    {
        return mInstrumentation;
    }
  • 1
  • 2
  • 3
  • 4
  • 5

所以我们继续查看Instrumentation 对象的execStartActivity方法

//Instrumentation.java
 public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {
                 //...省略方法其他方法
 int result = ActivityManagerNative.getDefault().startActivity(一堆参数);

//...     
        return null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面的代码就是传入的参数特别的参数太多。所以用四个字代替。我们看到ActivityManagerNative.getDefault()获取一个对象然后调用startActivity()

ActivityManagerNative.getDefault()方法探究

//ActivityManagerNative.java
  static public IActivityManager getDefault() {
        return gDefault.get();
    }
  • 1
  • 2
  • 3
  • 4

gDefaultActivityManagerNative.java属性对象

声明如下:

//ActivityManagerNative.java
  private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Singleton又是什么?这个是一个单例工具类,当你第一次调用此类的get()会回调使用则自己实现的抽象方法create()进而进行单例操作

此类完整代码:

//Singleton.java
package android.util;
public abstract class Singleton<T> {
    private T mInstance;

    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这类我们以后反射其拿到IActivityManager 大家可以简单可以

大家再回过头看看实现抽象的方法create()

//ActivityManagerNative.java
  private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            //从系统服务管理器获取activityMnagerServer(这是一个系统服务,所以需要AIDL进行进程间通信)
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
           //利用IBinder 对象转化为对应实例化接口
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从上面的例子可以看到,看到ActivityManagerNative.getDefault()返回了远程服务对象IActivityManager 接口。

再回头看看Instrumentation 对象的execStartActivity方法

//Instrumentation.java
 public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {
                 //...省略方法其他方法
 int result = ActivityManagerNative.getDefault().startActivity(一堆参数);

//...     
        return null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以得出以下结论:获取一个远程服务对象接口IActivityManager(并且是单例)然后调用IActivityManagerstartActivity方法。此时会回调到远程服务ActivityManagerServer的真正startActivity接口方法。里面会检查你传入的Intent对象的Activity是否在清单内,如果你的activity不在清单内,就是一个异常.但是我们不需要再看ActivityManagerServer源码,因为它是另一个app进程,所以无法干涉。如果可以干涉的话,Android系统就太不稳定了,肯定会有流氓开发者,让你按上一个app干扰其他app那不是奔了?所以我们无法hook到远程服务,但是IActivityManager我们却是可以用手脚的。因为那是服务端存在客户端一个代理接口对象、(这一块需要大家简单了解下AIDL的知识。如果不理解没关系,只需记住它做些手脚)

看完上面的很乱吧,看下图来屡屡思路:
(大家可以右键保存看大图)
这是个图片
这时候读者一定会问?既然ActivityManagerServer启动activity我们无法干涉那么后面干嘛?

我们仔细看上面的图IActivityManager调用的时候会把要启动的ActivityIntent去给ActivityManagerServer,假设我们此时用动态代理在IActivityManager调用远程服务之前,把在一个在清单文件注册过的ActivityIntent替换不就通过校验了?当然这时候你又会想既然替换通过了验证,但是我们真正要启动的Activity都被替了哪还有什么意义?这里过验证后系统没有真正启动Activity,因为这个ActivityManagerServer最终回调到* *启动的 。这里大家先不要着急,先通过验证再说。

第一个小目标 实现替换

假设我们我们有3个Activity第一个为MainActivity,第二个PlaceholdActivity 第三个为SecondActivity

  • MainActivity 在清单文件注册过,继承哪个Activity都无所谓

  • PlaceholdActivity 在清单文件注册过,继承哪个Activity都无所谓

  • SecondActivity没有清单文件注册过,并且继承Activity。(不要继承不然会失败AppCompatActivity)

其中我们在MainActivity 点击一个按钮触发其onclick方法调用startActivity跳转到SecondActivity

//MainActivity.java 
    public void onClick(View view) {
        Intent intent = new Intent(this,SecondActivity.class);
        startActivity(intent);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

按照常理因为SecondActivity没有在清单文件注册过所以会失败,我们的目的把这个意图换成启动PlaceholdActivity 的意图

步骤1

创建一个Application类名为APP.java(记得在清单文件中关联哦)
重写attachBaseContext方法

  • attachBaseContext方法介绍:
    此方法会在oncreate方法前调用,还记得我们说的ContextWrapperContextImpl吗?再回过头看看吧(application也是继承ContextWrapper的)。
public class ContextWrapper extends Context {
    Context mBase;

    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

attachBaseContext是在传入mBase对象赋值的回调的。此时你其实就可以使用上下文了。

此时我们的代码如下:

//APP.java
public class APP extends Application {
    //后面有用,用于保存原目标Activity的Intent的KEY 大家先看看
    private static final String KEY_EXTRA_TARGET_INTENT = "EXTRA_TARGET_INTENT";
    private static final String TAG = "APP";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我们前面分析知道想要在过Activity的清单文件校验,就必须在IActivityManager调用startActivity方法之前进行Intent意图替换。
所以我们直接用动态代理IActivityManagerIActivityManager

首先要动态代理你必须拿到IActivityManager对象。而此对象保存在ActivityManagerNative的属性对象gDefault中。我们调用属性对象gDefault(Singleton类)的get方法或者得到其内部属性对象mInstance

mInstance是用于保存一个单例对象的实例。很混乱?再回头看看Singleton

//Singleton.java
public abstract class Singleton<T> {
      //保存对象实例
    private T mInstance;
    //Singleton对象为空会调用此方法得到实例
    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

好了先获取gDefault对象吧,我们看看ActivityManagerNative对其的声明

//ActivityManagerNative.java
  private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我们发现是个静态类,所以我们应该笑了,静态类通过反射机制很容易得到对象实例

  • 所以得到如下代码:
public class APP extends Application {
    private static final String KEY_EXTRA_TARGET_INTENT = "EXTRA_TARGET_INTENT";
    private static final String TAG = "APP";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        try {
            Class<?> ams_class = Class.forName("android.app.ActivityManagerNative");
            Field gDefault = ams_class.getDeclaredField("gDefault");
            //因为gDefault是private所以用发射机制打破访问限制
            gDefault.setAccessible(true);
            //得到gDefault实例对象 因为是stati属性所以get传入null即可得到
            Object gDefault_instance = gDefault.get(null);
            //单例工具类class
            Class<?> singleton_class = Class.forName("android.util.Singleton");
            //单例工具类的一个属性对象保存的是IActivityManager对象实例
            Field mInstance = singleton_class.getDeclaredField("mInstance");
            //打破封装访问
            mInstance.setAccessible(true);
            //传入gDefault对象实例得到IActivityManager对象实例
            final Object mInstance_instance = mInstance.get(gDefault_instance);
            //动态代理,这个不会我真不知道讲下去。此方法会返回一个实现IActivityManager接口的实例对象
            //拿着这个对象 替换单例工具类gDefault 的mInstance即可实现
            Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), mInstance_instance.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                   // IActivityManager接口方法太多 我们只关心它启动activity的接口方法
                    if (method.getName().equals("startActivity")) {
                        //获取调用此方法传入的参数 我们这里只要Intent替换,所以只需要Intent替换
                        for (int i = 0; i < args.length; i++) {
                            Object arg = args[i];
                            if (arg instanceof Intent) {
                                Intent intent = new Intent();
                                //PlaceholdActivity因为在清单文件注册过所以 创建一个新的Intent替换原来的SecondActivity的意图。后面用到
                                // 并将其保存到新的Intent中
                                ComponentName componentName = new ComponentName("com.example.fmy.myapplication", PlaceholdActivity.class.getName());
                                intent.setComponent(componentName);
                                intent.putExtra(KEY_EXTRA_TARGET_INTENT, ((Intent) arg));
                                args[i] = intent;
                                return method.invoke(mInstance_instance, args);
                            }
                        }

                    }

                    return method.invoke(mInstance_instance, args);
                }
            });
//         替换单例工具类gDefault 的mInstance实现动态代理
            mInstance.set(gDefault_instance, proxyInstance);
        } catch (Exception e) {
            e.printStackTrace();
        }
     }
}
  • 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

你运行代码后发现不管startActivity填入什么意图都是启动PlaceholdActivity就证明你成功了

Activity启动流程分析之Handler源码分析 第二章

这里只会简单讲讲不会过多。不然后脱离主旨
Looper循环器,不断从对应线程MassgaeQueue中死循环拿取mesaage然后发送到对应的handler

handler有消息的会调用如下方法:

    //Handler.java
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //如果mCallback 不为空并且处理后返回true 那么不会调用我们自己写handlerMessage处理消息。
            handleMessage(msg);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 我们后面会用此mCallback 就行再次替换Intent

我们知道在学习javase的时候我们程序是从main方法进入,那么安卓的app入门方法在哪?ActivityThread.java中存在的main方法就是

//ActivityThread.java
public static void main(String[] args) {

        //...代码省略
        //Looper类一些初始化,会从ThreadLocal对象拿取当前线程的Looper
        //关于ThreadLocal不做过多讲述,简单就是用线程资源拷贝。
        //假设你有个变量a 你可以拷贝三份到ThreadLocal中不同的线程 
        Looper.prepareMainLooper();
        //...代码省略 
        ActivityThread thread = new ActivityThread();
         //开启一个子线程,又可以叫binder线程用于ams(ActivityManagerServer通AIDL来进程间通信,如果有信息那么从子线程发送message给主线程handler进行处理)
        thread.attach(false);

       //...代码省略
           //开启一个死循环遍历messageQueue(里面存放messager),如果有就,所以前面才开启一个子线程,不然怎么跟AMS通信?
        Looper.loop();

       //...代码省略
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

看看 thread.attach(false);方法

//ActivityThread.java
  private void attach(boolean system) {
        sCurrentActivityThread = this;
          //...忽略代码
      }
  • 1
  • 2
  • 3
  • 4
  • 5

我们只看到方法 把当前线程对象(ActivityThread)保存到sCurrentActivityThread 我们后面会用到所以这里讲解。

我们前面第一章分析中到通过IActivityManager发送startActivity方法到ActivityManagerServer中,然后我们知道知道在ActivityThread类中main方法死循环前开启了个子线程,这个线程会接收ActivityManagerServer回馈的信息,信息封装在Message中然后添加主线程MeassgeQueue中,当主线程的Loop遍历到有信息的时候交给主线程的Handler处理。

  • ActivityManagerServer收到startActivity信息的信息的时候,会发送一个messageHandler处理。其中message.what=LAUNCH_ACTIVITYLAUNCH_ACTIVITY=100

我们看看ActivityHandler在哪

public class AcitvityThread{
    final H mH = new H();
    private class H extends Handler {
    //。。。代码省略
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们在看看mH这个Handler怎么处理private class H extends Handler 的事件

//ActivityThread.java
class H extends Handler{
//.....
public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                   //从msg拿出ActivityClientRecord 对象,里面包含启动activity的Intent
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                            //带着ActivityClientRecord 对象启动activity
                    handleLaunchActivity(r, null);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

大致可简单的理解从msg获取ActivityClientRecord 对象,内包含启动Intent启动意图等信息,还记得我们前面保存一个Intent到一个新的Intent上吗?这时候其实你可以从这个Intent取出来了。调用handleLaunchActivity去启动,最后大家想生命周期可以自己往下深究。

//ActivityThread.java
    static final class ActivityClientRecord {
        IBinder token;
        int ident;
        Intent intent;
        String referrer;
        //....
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

先来整理回调流程:
(因为懒省略很多细节)

这里写图片描述

所以我们可以handlermCallback赋值,此时handler回调lancherActivity的时候,我们知道mCallback不为空的时候,先会回调mCallback再根据其返回值在判断要不要对其自己实现的handlerMessagetrue的时候不会调用自己实现的方法)

所以先获取handler对象,而handler对象存在ActivityThread中,再来看看ActivityThread.java对其的声明

//ActivityThread.java
public final class ActivityThread {
 final H mH = new H();
 }
  • 1
  • 2
  • 3
  • 4

因为只mH不是静态类型,所以先要获取ActivityThread 对象才能获取mH实例。
还记得我们前面分析ActivityThreadmain方法吗?
内部调用

public static void main(String[] args) {
//...
 ActivityThread thread = new ActivityThread();
        thread.attach(false);
    //...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

再看看 thread.attach(false);方法

//ActivityThread.java
private void attach(boolean system) {
        sCurrentActivityThread = this;
        //....
        }
  • 1
  • 2
  • 3
  • 4
  • 5

可以看到ActivityThread对象保存在ActivityThread的属性对象sCurrentActivityThread

//ActivityThread.java
public final class ActivityThread {
 //...
  private static ActivityThread sCurrentActivityThread;
//...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

静态属性证明你懂的,你可以利用反射机制获取对象

所以综上代码综合可得:

//APP.java
package com.example.fmy.myapplication;

public class APP extends Application {
    private static final String KEY_EXTRA_TARGET_INTENT = "EXTRA_TARGET_INTENT";
    private static final String TAG = "APP";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

            //过检验代码省略
            Class<?> activityThread_class = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThread_field = activityThread_class.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThread_field.setAccessible(true);
            //获取静态属性对象实例
            Object activityThread_instance = sCurrentActivityThread_field.get(null);

            Field mH_field = activityThread_class.getDeclaredField("mH");

            mH_field.setAccessible(true);
            //获取handler对象实例
            Object mH_insance = mH_field.get(activityThread_instance);

            Field mCallback_field = Handler.class.getDeclaredField("mCallback");


            mCallback_field.setAccessible(true);
            //给handler添加mCallback
            mCallback_field.set(mH_insance, new Handler.Callback() {

                @Override
                public boolean handleMessage(Message msg) {
                    if (msg.what == 100) {
                        //得到ActivityClientRecord
                        Object obj = msg.obj;
                        try {
                            //得到Intent对象
                            Field intent_field = obj.getClass().getDeclaredField("intent");
                            intent_field.setAccessible(true);
                            Intent intent = (Intent) intent_field.get(obj);
                            //取出我们前面存在Intent里的原本没有注册在清单文件的Activity的Intent
                            Intent target_intent = intent.getParcelableExtra(KEY_EXTRA_TARGET_INTENT);

                            if (target_intent != null) {
                                intent.setComponent(target_intent.getComponent());
                            }

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

                    }
                    return false;

                }
            });

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


    }

    @Override
    public void onCreate() {
        super.onCreate();
    }
}
  • 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

最后整合下过验证的代码如下:

//APP.java
package com.example.fmy.myapplication;

public class APP extends Application {
    private static final String KEY_EXTRA_TARGET_INTENT = "EXTRA_TARGET_INTENT";
    private static final String TAG = "APP";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        try {
            Class<?> ams_class = Class.forName("android.app.ActivityManagerNative");
            Field gDefault = ams_class.getDeclaredField("gDefault");
            //因为gDefault是private所以用发射机制打破访问限制
            gDefault.setAccessible(true);
            //得到gDefault实例对象 因为是stati属性所以get传入null即可得到
            Object gDefault_instance = gDefault.get(null);
            //单例工具类class
            Class<?> singleton_class = Class.forName("android.util.Singleton");
            //单例工具类的一个属性对象保存的是IActivityManager对象实例
            Field mInstance = singleton_class.getDeclaredField("mInstance");
            //打破封装访问
            mInstance.setAccessible(true);
            //传入gDefault对象实例得到IActivityManager对象实例
            final Object mInstance_instance = mInstance.get(gDefault_instance);
            //动态代理,这个不会我真不知道讲下去。此方法会返回一个实现IActivityManager接口的实例对象
            //拿着这个对象 替换单例工具类gDefault 的mInstance即可实现
            Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), mInstance_instance.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    // IActivityManager接口方法太多 我们只关心它启动activity的接口方法
                    if (method.getName().equals("startActivity")) {
                        //获取调用此方法传入的参数 我们这里只要Intent替换,所以只需要Intent替换
                        for (int i = 0; i < args.length; i++) {
                            Object arg = args[i];
                            if (arg instanceof Intent) {
                                Intent intent = new Intent();
                                //PlaceholdActivity因为在清单文件注册过所以 创建一个新的Intent替换原来的SecondActivity的意图。后面用到
                                // 并将其保存到新的Intent中
                                ComponentName componentName = new ComponentName("com.example.fmy.myapplication", PlaceholdActivity.class.getName());
                                intent.setComponent(componentName);
                                intent.putExtra(KEY_EXTRA_TARGET_INTENT, ((Intent) arg));
                                args[i] = intent;
                                return method.invoke(mInstance_instance, args);
                            }
                        }

                    }

                    return method.invoke(mInstance_instance, args);
                }
            });
//         替换单例工具类gDefault 的mInstance实现动态代理
            mInstance.set(gDefault_instance, proxyInstance);

            //过检验结束
            Class<?> activityThread_class = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThread_field = activityThread_class.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThread_field.setAccessible(true);
            //获取静态属性对象实例
            Object activityThread_instance = sCurrentActivityThread_field.get(null);

            Field mH_field = activityThread_class.getDeclaredField("mH");

            mH_field.setAccessible(true);
            //获取handler对象实例
            Object mH_insance = mH_field.get(activityThread_instance);

            Field mCallback_field = Handler.class.getDeclaredField("mCallback");


            mCallback_field.setAccessible(true);
            //给handler添加mCallback
            mCallback_field.set(mH_insance, new Handler.Callback() {

                @Override
                public boolean handleMessage(Message msg) {
                    if (msg.what == 100) {
                        //得到ActivityClientRecord
                        Object obj = msg.obj;
                        try {
                            //得到Intent对象
                            Field intent_field = obj.getClass().getDeclaredField("intent");
                            intent_field.setAccessible(true);
                            Intent intent = (Intent) intent_field.get(obj);
                            //取出我们前面存在Intent里的原本没有注册在清单文件的Activity的Intent
                            Intent target_intent = intent.getParcelableExtra(KEY_EXTRA_TARGET_INTENT);

                            if (target_intent != null) {
                                intent.setComponent(target_intent.getComponent());
                            }

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

                    }
                    return false;

                }
            });

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


    }

    @Override
    public void onCreate() {
        super.onCreate();
    }
}
  • 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

GIT源码附上源码连接

Handler源码分析

虽然不是自己写的 但是写的很棒所以转载到此

Android 消息处理机制估计都被写烂了,但是依然还是要写一下,因为Android应用程序是通过消息来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关,并且消息处理机制在整个Android知识体系中也是尤其重要,在太多的源码分析的文章讲得比较繁琐,很多人对整个消息处理机制依然是懵懵懂懂,这篇文章通过一些问答的模式结合Android主线程(UI线程)的工作原理来讲解,源码注释很全,还有结合流程图,如果你对Android 消息处理机制还不是很理解,我相信只要你静下心来耐心的看,肯定会有不少的收获的。

概述


1、我们先说下什么是Android消息处理机制?

消息处理机制本质:一个线程开启循环模式持续监听并依次处理其他线程给它发的消息。

简单的说:一个线程开启一个无限循环模式,不断遍历自己的消息列表,如果有消息就挨个拿出来做处理,如果列表没消息,自己就堵塞(相当于wait,让出cpu资源给其他线程),其他线程如果想让该线程做什么事,就往该线程的消息队列插入消息,该线程会不断从队列里拿出消息做处理。

2、Android消息处理机制的工作原理?

打个比方:公司类比App

  • PM 的主要工作是设计产品,写需求文档,改需求,中途改需求,提测前改需求…
  • UI 主要工作是UI设计,交互等。
  • RD 工作我就不说了
  • CEO 不解释。

公司开创之后(App启动),那么CEO开始干活了(主线程【UI线程】启动),这时候CEO开启了无限循环工作狂模式,自己的公司没办法啊(相当于UI主线程转成Looper线程【源码里面有】)CEO招了一名RD(new Handler 实例)并把告诉PM和UI,如果你们有什么任务和需求就让RD(Handler实例)转告给我(CEO)。RD会把PM和UI的需求(Message)一条条记到CEO的备忘录里(MessageQueue)。CEO 无限循环的工作就是不断查看备忘录,看有什么任务要做,有任务就从备忘录一条一条拿出任务来,然后交给这一名RD(Handler 实例)去处理(毕竟CEO 不会写代码 囧…)。当然如果备忘录都做完了,这时候CEO就会去睡觉(线程堵塞【简单理解成线程wait】,让出CPU资源,让其他线程去执行)。但是这个备忘录有个特殊的功能就是没有任务的时候突然插入第一条任务(从无到有)就会有闹钟功能叫醒CEO起床继续处理备忘录。 整个消息处理机制的工作原理基本就是这样的。后面会有源码分析,你再来结合这个场景,会更好理解一些。

这里先给一张Android消息处理机制流程图和具体执行动画,如果看不懂没事,接着往下看(后面会结合Android UI主线程来讲解),然后结合着图和动画一块看更能理解整个机制的实现原理。

3、LooperHandlerMessageQueue,Message作用和存在的意义?

  • Looper
    我们知道一个线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便会终止,线程就会退出,那么做为App的主线程,如果代码段执行完了会怎样?,那么就会出现App启动后执行一段代码后就自动退出了,这是很不合理的。所以为了防止代码段被执行完,只能在代码中插入一个死循环,那么代码就不会被执行完,然后自动退出,怎么在在代码中插入一个死循环呢?那么Looper出现了,在主线程中调用Looper.prepare()...Looper.loop()就会变当前线程变成Looper线程(可以先简单理解:无限循环不退出的线程),Looper.loop()方法里面有一段死循环的代码,所以主线程会进入while(true){...}的代码段跳不出来,但是主线程也不能什么都不做吧?其实所有做的事情都在while(true){...}里面做了,主线程会在死循环中不断等其他线程给它发消息(消息包括:Activity启动,生命周期,更新UI,控件事件等),一有消息就根据消息做相应的处理,Looper的另外一部分工作就是在循环代码中会不断从消息队列挨个拿出消息给主线程处理。

  • MessageQueue
    MessageQueue 存在的原因很简单,就是同一线程在同一时间只能处理一个消息,同一线程代码执行是不具有并发性,所以需要队列来保存消息和安排每个消息的处理顺序。多个其他线程往UI线程发送消息,UI线程必须把这些消息保持到一个列表(它同一时间不能处理那么多任务),然后挨个拿出来处理,这种设计很简单,我们平时写代码其实也经常这么做。每一个Looper线程都会维护这样一个队列,而且仅此一个,这个队列的消息只能由该线程处理。

  • Handler
    简单说Handler用于同一个进程的线程间通信。Looper让主线程无限循环地从自己的MessageQueue拿出消息处理,既然这样我们就知道处理消息肯定是在主线程中处理的,那么怎样在其他的线程往主线程的队列里放入消息呢?其实很简单,我们知道在同一进程中线程和线程之间资源是共享的,也就是对于任何变量在任何线程都是可以访问和修改的,只要考虑并发性做好同步就行了,那么只要拿到MessageQueue 的实例,就可以往主线程的MessageQueue放入消息,主线程在轮询的时候就会在主线程处理这个消息。那么怎么拿到主线程 MessageQueue的实例,是可以拿到的(在主线程下mLooper = Looper.myLooper();mQueue = mLooper.mQueue;),但是Google 为了统一添加消息和消息的回调处理,又专门构建了Handler类,你只要在主线程构建Handler类,那么这个Handler实例就获取主线程MessageQueue实例的引用(获取方式mLooper = Looper.myLooper();mQueue = mLooper.mQueue;),Handler 在sendMessage的时候就通过这个引用往消息队列里插入新消息。Handler 的另外一个作用,就是能统一处理消息的回调。这样一个Handler发出消息又确保消息处理也是自己来做,这样的设计非常的赞。具体做法就是在队列里面的Message持有Handler的引用(哪个handler 把它放到队列里,message就持有了这个handler的引用),然后等到主线程轮询到这个message的时候,就来回调我们经常重写的Handler的handleMessage(Message msg)方法。

  • Message
    Message 很简单了,你想让主线程做什么事,总要告诉它吧,总要传递点数据给它吧,Message就是这个载体。

源码分析


接下来我们会结合App主线程(UI线程)来讲解,从App启动后一步一步往下走分析整个Android的消息处理机制,首先在ActivityThread类有我们熟悉的main的函数,App启动的代码的入口就在这里,UI线程本来只是一个普通线程,在这里会把UI线程转换成Looper线程,什么是Looper线程,不急往下看就知道了。

public final class ActivityThread {
    public static final void main(String[] args) {
        ......
        Looper.prepareMainLooper();
        ......
        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {    
            sMainThreadHandler = thread.getHandler();
        }
        ......
        Looper.loop();
        ......
    }
}

首先执行的是 Looper.prepareMainLooper() 我们来看下Looper里面的这个方法做了什么?

注:看之前先稍微了解下ThreadLocal是什么?
ThreadLocal: 线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。这里线程自己的本地存储区域存放是线程自己的Looper。具体看下ThreadLocal.java 的源码!

public final class Looper {
    // sThreadLocal 是static的变量,可以先简单理解它相当于map,key是线程,value是Looper,
    //那么你只要用当前的线程就能通过sThreadLocal获取当前线程所属的Looper。
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    //主线程(UI线程)的Looper 单独处理,是static类型的,通过下面的方法getMainLooper() 
    //可以方便的获取主线程的Looper。
    private static Looper sMainLooper; 

    //Looper 所属的线程的消息队列
    final MessageQueue mQueue;
    //Looper 所属的线程
    final Thread mThread;

    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
         //如果线程的TLS已有数据,则会抛出异常,一个线程只能有一个Looper,prepare不能重复调用。
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //往线程的TLS插入数据,简单理解相当于map.put(Thread.currentThread(),new Looper(quitAllowed));
        sThreadLocal.set(new Looper(quitAllowed));
    }

    //实际上是调用  prepare(false),并然后给sMainLooper赋值。
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    //static 方法,方便获取主线程的Looper.
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

    public static @Nullable Looper myLooper() {
        //具体看ThreadLocal类的源码的get方法,
        //简单理解相当于map.get(Thread.currentThread()) 获取当前线程的Looper
        return sThreadLocal.get();
    }
}

看了上面的代码(仔细看下注释),我们发现 Looper.prepareMainLooper()做的事件就是new了一个Looper实例并放入Looper类下面一个static的ThreadLocal<Looper> sThreadLocal静态变量中,同时给sMainLooper赋值,给sMainLooper赋值是为了方便通过Looper.getMainLooper()快速获取主线程的Looper,sMainLooper是主线程的Looper可能获取会比较频繁,避免每次都到 sThreadLocal 去查找获取。

接下来重点是看下Looper的构造函数,看看在new Looper的时候做了什么事?

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
}

看到没有,这时候给当前线程创建了消息队列MessageQueue,并且让Looper持有MessageQueue的引用。执行完Looper.prepareMainLooper() 之后,主线程从普通线程转成一个Looper线程。目前的主线程线程已经有一个Looper对象和一个消息队列mQueue,引用关系如下图:(主线程可以轻松获取它的Looper,主线程的Looper持有主线程消息队列的引用)

具体如何获取主线程的Looper对象和消息列表呢?

//在主线程中执行
mLooper = Looper.myLooper();
mQueue = mLooper.mQueue
//或者
mLooper=Looper.getMainLooper()

接下来回到ActivityThread 的main函数,执行完Looper.prepareMainLooper() 之后下一句代码是ActivityThread thread = new ActivityThread();这句话就是创建一下ActivityThread对象,这边需要注意的时候ActivityThread并不是一个线程,它并没有继承Thread,而只是一个普通的类public final class ActivityThread{...}ActivityThread的构造函数并没有做什么事只是初始化了资源管理器。

 ActivityThread() {
     mResourcesManager = ResourcesManager.getInstance();
 }

接着往下看下一行代码

ActivityThread thread = new ActivityThread();
//建立Binder通道 (创建新线程)
thread.attach(false);

thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,该Binder线程会通过想 HandlerMessage发送给主线程,之后讲)。我们之前提到主线程最后会进入无限循环当中,如果没有在进入死循环之前创建其他线程,那么待会谁会给主线程发消息呢?,没错就是在这里创建了这个线程,这个线程会接收来自系统服务发送来的一些事件封装了Message并发送给主线程,主线程在无限循环中从队列里拿到这些消息并处理这些消息。(Binder线程发生的消息包括LAUNCH_ACTIVITYPAUSE_ACTIVITY 等等)

继续回到mian 函数的下一句代码Looper.loop() 那么重点来了,我们来看下Looper.loop()的源码:

public static void loop() {
    final Looper me = myLooper();  //获取TLS存储的Looper对象,获取当前线程的Looper 
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }

    final MessageQueue queue = me.mQueue;  //获取Looper对象中的消息队列
    ....

    for (;;) { //主线程开启无限循环模式
        Message msg = queue.next(); //获取队列中的下一条消息,可能会线程阻塞
        if (msg == null) { //没有消息,则退出循环,退出消息循环,那么你的程序也就可以退出了
            return;
        }
        ....
        //分发Message,msg.target 是一个Handler对象,哪个Handler把这个Message发到队列里,
        //这个Message会持有这个Handler的引用,并放到自己的target变量中,这样就可以回调我们重写
        //的handler的handleMessage方法。
        msg.target.dispatchMessage(msg);
        ....
        ....
        msg.recycleUnchecked();  //将Message回收到消息池,下次要用的时候不需要重新创建,obtain()就可以了。
    }
}

上面的代码,大家具体看下注释,这时候主线程(UI线程)执行到这一步就进入了死循环,不断地去拿消息队列里面的消息出来处理?那么问题来了
1、UI线程一直在这个循环里跳不出来,主线程不会因为Looper.loop()里的死循环卡死吗,那还怎么执行其他的操作呢?

  • 在looper启动后,主线程上执行的任何代码都是被looper从消息队列里取出来执行的。也就是说主线程之后都是通过其他线程给它发消息来实现执行其他操作的。生命周期的回调也是如此的,系统服务ActivityManagerService通过Binder发送IPC调用给APP进程,App进程接到到调用后,通过App进程的Binder线程给主线程的消息队列插入一条消息来实现的。

2、主线程是UI线程和用户交互的线程,优先级应该很高,主线程的死循环一直运行是不是会特别消耗CPU资源吗?App进程的其他线程怎么办?

  • 这基本是一个类似生产者消费者的模型,简单说如果在主线程的MessageQueue没有消息时,就会阻塞在loop的queue.next()方法里,这时候主线程会释放CPU资源进入休眠状态,直到有下个消息进来时候就会唤醒主线程,在2.2 版本以前,这套机制是用我们熟悉的线程的wait和notify 来实现的,之后的版本涉及到Linux pipe/epoll机制,通过往pipe管道写端写入数据来唤醒主线程工作。原理类似于I/O,读写是堵塞的,不占用CPU资源。

所以上面代码的重点是queue.next() 的函数,其他的我们就不多说了,我们来看下queue.next()的源码(主要还是看注释):

Message next() 

        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        int pendingIdleHandlerCount = -1; // -1 only during first iteration

        //nextPollTimeoutMillis 表示nativePollOnce方法需要等待nextPollTimeoutMillis 
        //才会返回
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            //读取消息,队里里没有消息有可能会堵塞,两种情况该方法才会返回(代码才能往下执行)
            //一种是等到有消息产生就会返回,
            //另一种是当等了nextPollTimeoutMillis时长后,nativePollOnce也会返回
            nativePollOnce(ptr, nextPollTimeoutMillis);
            //nativePollOnce 返回之后才能往下执行
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // 循环找到一条不是异步而且msg.target不为空的message
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                       // 虽然有消息,但是还没有到运行的时候,像我们经常用的postDelay,
                       //计算出离执行时间还有多久赋值给nextPollTimeoutMillis,
                       //表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 获取到消息
                        mBlocked = false;
                       //链表一些操作,获取msg并且删除该节点 
                        if (prevMsg != null) 
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        //返回拿到的消息
                        return msg;
                    }
                } else {
                    //没有消息,nextPollTimeoutMillis复位
                    nextPollTimeoutMillis = -1;
                }
                .....
                .....

    }

nativePollOnce()很重要,是一个native的函数,在native做了大量的工作,主要涉及到epoll机制的处理(在没有消息处理时阻塞在管道的读端),具体关于native相关的源码本篇文章不涉及,感兴趣的同学可以网上找找,有不少分析得比较深。

分析到这里,从应用启动创建Looper,创建消息队列,到进入loop方法执行无限循环中,那么这一块就告一段落了,主线程已经在死循环里轮询等待消息了,接下来我们就要再看看,系统是怎么发消息给主线程的,主线程是怎么处理这些个消息的?

在准备启动一个Activity的时候,系统服务进程下的ActivityManagerService(简称AMS)线程会通过Binder发送IPC调用给APP进程,App进程接到到调用后,通过App进程下的Binder线程最终调用ActivityThread类下面的scheduleLaunchActivity方法来准备启动Activity,看下scheduleLaunchActivity方法:

注:Binder线程:具体是指ApplicationThread,在App进程中接受系统进程传递过来的信息的线程(在主线程进入死循环之前创建了这个线程)。

  //这个方法不是在主线程调用,是Binder线程下调用的
  public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

            updateProcessState(procState, false);

            ActivityClientRecord r = new ActivityClientRecord();

            r.token = token;
            r.ident = ident;
            r.intent = intent;
            r.referrer = referrer;
            r.voiceInteractor = voiceInteractor;
            r.activityInfo = info;
            r.compatInfo = compatInfo;
            r.state = state;
            r.persistentState = persistentState;

            r.pendingResults = pendingResults;
            r.pendingIntents = pendingNewIntents;

            r.startsNotResumed = notResumed;
            r.isForward = isForward;

            r.profilerInfo = profilerInfo;

            r.overrideConfig = overrideConfig;
            updatePendingConfiguration(curConfig);

            sendMessage(H.LAUNCH_ACTIVITY, r);
  }

把启动一些信息封装成ActivityClientRecord之后,最后一句调用sendMessage(H.LAUNCH_ACTIVITY, r);我们接着往下看:

private void sendMessage(int what, Object obj) {
        sendMessage(what, obj, 0, 0, false);
    }
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
         Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        if (async) {
            msg.setAsynchronous(true);
        }
        mH.sendMessage(msg);
    }

看到没有,最后启动Activity的信息都封装一个Message,但是这里有个问题了,之前在分析main函数的时候,完全没给出往主线程消息队列插入消息的方式,这里有了消息,但是怎么发到主线程的消息队列呢?最后一句又是重点mH.sendMessage(msg); mH 是什么呢?难道是Handler,我们来看下它是什么东西?
我们看了下ActivityThread 的成员变量,发现一句初始化的代码

final H mH = new H();

继续往下看H是什么?

public final class ActivityThread{
     ....
     final H mH = new H();
     ....
     private class H extends Handler {
     ....
     ....
     public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                case RELAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
                    ActivityClientRecord r = (ActivityClientRecord)msg.obj;
                    handleRelaunchActivity(r);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                case PAUSE_ACTIVITY:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
                    handlePauseActivity((IBinder)msg.obj, false, (msg.arg1&1) != 0, msg.arg2,
                            (msg.arg1&2) != 0);
                    maybeSnapshot();
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                   .....
         }
         .....
         .....
     }
}

H 果不出其然是Handler,而且是ActivityThread的内部类,看了一下它的handleMessage 方法,LAUNCH_ACTIVITYPAUSE_ACTIVITYRESUME_ACTIVITY…好多好多,H 类帮我们处理了好多声明周期的事情。那么再回到mH.sendMessage(msg)这句代码上,在Binder线程执行mH.sendMessage(msg);,由主线程创建的Handler mH实例发送消息到主线程的消息队列里,消息队列从无到有,主线程堵塞被唤醒,主线程loop拿到消息,并回调mHhandleMessage 方法处理LAUNCH_ACTIVITY 等消息。从而实现Activity的启动。

讲到这里,基本一个启动流程分析完了,大家可能比较想知道的是 mH.sendMessage(msg); 关于Hanlder是怎么把消息发到主线程的消息队列的?我们接下来就讲解下Handler,首先看下Handler的源码!我们先来看看我们经常用的Handler的无参构造函数,实际调用的是Handler(Callback callback, boolean async)构造函数(看注释)

 public Handler() {
        this(null, false);
 }
 public Handler(Callback callback, boolean async) {
        //不是static 发出可能内存泄露的警告!
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        //获取当前线程的Looper,还记得前面讲过 Looper.myLooper()方法了吗?
        //Looper.myLooper()内部实现可以先简单理解成:map.get(Thread.currentThread()) 
        //获取当前线程的Looper
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            //当前线程不是Looper 线程,没有调用Looper.prepare()给线程创建Looper对象
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //让Handler 持有当前线程消息队列的引用
        mQueue = mLooper.mQueue;
        //这些callback先不管,主要用于handler的消息发送的回调,优先级是比handlerMessage高,但是不常用
        mCallback = callback;
        mAsynchronous = async;
    }

上面的代码说明了下面几个问题:
1、Handler 对象在哪个线程下构建(Handler的构造函数在哪个线程下调用),那么Handler 就会持有这个线程的Looper引用和这个线程的消息队列的引用。因为持有这个线程的消息队列的引用,意味着这个Handler对象可以在任意其他线程给该线程的消息队列添加消息,也意味着Handler的handlerMessage 肯定也是在该线程执行的。
2、如果该线程不是Looper线程,在这个线程new Handler 就会报错!
3、上面两点综合说明了下面一段很常见的代码:把普通线程转成Looper线程的代码,为什么在Looper.prepare()Looper.loop()中间要创建Handler:

 class LooperThread extends Thread {
       //其他线程可以通过mHandler这个引用给该线程的消息队列添加消息
       public Handler mHandler;
       public void run() {
            Looper.prepare();
            //需要在线程进入死循环之前,创建一个Handler实例供外界线程给自己发消息
            mHandler = new Handler() {
                public void handleMessage(Message msg) {
                    //Handler 对象在这个线程构建,那么handleMessage的方法就在这个线程执行
                }
            };
            Looper.loop();
        }
    }

那么接下来,我们接着往下看Handler的sendMessage(msg)方法,这个方法也是比较重要的,也比较常用,Handler 有很多sendXXXX开头的方法sendMessageAtTimesendEmptyMessageDelayedsendEmptyMessage等等,都是用来给消息队列添加消息的,那么这些方法最终都会调用enqueueMessage来实现消息进入队列:

 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        //这句话很重要,让消息持有当前Handler的引用,在消息被Looper线程轮询到的时候
        //回调handler的handleMessage方法
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        //调用MessageQueue 的enqueueMessage 方法把消息放入队列
        return queue.MessageQueue(msg, uptimeMillis);
    }

我们再来看下MessageQueue 的enqueueMessage(msg, uptimeMillis)方法:

    boolean enqueueMessage(Message msg, long when) {
        // msg 必须有target也就是必须有handler
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
        //插入消息队列的时候需要做同步,因为会有多个线程同时做往这个队列插入消息
        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            //when 表示这个消息执行的时间,队列是按照消息执行时间排序的
            //如果handler 调用的是postDelay 那么when=SystemClock.uptimeMillis()+delayMillis
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // p==null 表示当前消息队列没有消息
                msg.next = p;
                mMessages = msg;
                //需要唤醒主线程,如果队列没有元素,主线程会堵塞在管道的读端,这时
                //候队列突然有消息了,就会往管道写入字符,唤醒主线程
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //将消息放到队列的确切位置,队列是按照msg的when 排序的,链表操作自己看咯
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // 如果需要唤醒Looper线程,这里调用native的方法实现epoll机制唤醒线程,我们就不在深入探讨了
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

最后我们再看下Handler 的dispatchMessage方法,这个方法在Looper线程从消息队列拿出来的时候,通过msg.target.dispatchMessage(msg)调用的。

 /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        //优先调用callback方法
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //最后会回调我们重写的handleMessage 方法
            handleMessage(msg);
        }
    }

到这里,整个Android的消息处理机制Java层内容基本讲解完毕了(欢迎关注我的简书:Kelin)

注:【转载请注明,问题可留言,错误望指出,喜欢可打赏,博客持续更新,欢迎关注】

AIDL源码分析

前言

本文是本人研究AIDL时候的笔记,包含很多UML图和截图,内容仓促且不包含驱动层分析,如下文有错漏还请指出(容我精通Linux和C++后杀入,很可惜现在太菜)

服务端

  • 首先写一个AIDL文件 如下:
// IMyAidlInterface.aidl
package com.fmy.changevoice.aidl_resource;



interface IMyAidlInterface {

    void test1(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);

                String hello( String aString);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 在编译后生成一个java文件(as位于build/generated/source/aidl/debug),文件内容如下:

package com.fmy.changevoice.aidl_resource;

public interface IMyAidlInterface extends android.os.IInterface {

    public static abstract class Stub extends android.os.Binder implements com.fmy.changevoice.aidl_resource.IMyAidlInterface {
        private static final java.lang.String DESCRIPTOR = "com.fmy.changevoice.aidl_resource.IMyAidlInterface";


        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }


        public static com.fmy.changevoice.aidl_resource.IMyAidlInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.fmy.changevoice.aidl_resource.IMyAidlInterface))) {
                return ((com.fmy.changevoice.aidl_resource.IMyAidlInterface) iin);
            }
            return new com.fmy.changevoice.aidl_resource.IMyAidlInterface.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_test1: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    long _arg1;
                    _arg1 = data.readLong();
                    boolean _arg2;
                    _arg2 = (0 != data.readInt());
                    float _arg3;
                    _arg3 = data.readFloat();
                    double _arg4;
                    _arg4 = data.readDouble();
                    java.lang.String _arg5;
                    _arg5 = data.readString();
                    this.test1(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_hello: {
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    java.lang.String _result = this.hello(_arg0);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.fmy.changevoice.aidl_resource.IMyAidlInterface {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            /**
             * Demonstrates some basic types that you can use as parameters
             * and return values in AIDL.
             */
            @Override
            public void test1(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(anInt);
                    _data.writeLong(aLong);
                    _data.writeInt(((aBoolean) ? (1) : (0)));
                    _data.writeFloat(aFloat);
                    _data.writeDouble(aDouble);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_test1, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public java.lang.String hello(java.lang.String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_hello, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        static final int TRANSACTION_test1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_hello = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    public void test1(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException;

    public java.lang.String hello(java.lang.String aString) throws android.os.RemoteException;
}
  • 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
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 先看一下类图结构示例图:

这里写图片描述

  • 首先分析IMyAidlInterface这个类(内部类先不分析):
public interface IMyAidlInterface extends android.os.IInterface {
//内部类先不做分析
 public static abstract class Stub extends android.os.Binder implements com.fmy.changevoice.aidl_resource.IMyAidlInterface{...}
//声明接口方法
public void test1(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException;
//声明接口方法
    public java.lang.String hello(java.lang.String aString) throws android.os.RemoteException;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 我们看到IMyAidlInterface 还继承类一个接口IInterface ,我们再看看这个接口:
package android.os;

public interface IInterface
{


//检索与此接口相关联的Binder对象。您必须使用此代替普通转换,以便代理对象可以返回正确的结果。
    public IBinder asBinder();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 从描述可得治会在代理对象用使用使用此方法。然而IMyAidlInterface 没有实现此方法。我们继续看内部类Stub。
    这里写图片描述

  • 分析1:Stub是一个抽象类。继承Binder和实现IMyAidlInterface接口。
    但是从类图可以看到并没有实现我们IMyAidlInterface的方法(test1和hello方法)。

综上分析,我们服务实现接口的时候我们会继承IMyAidlInterface.Stub类。而不是直接IMyAidlInterface因为此接口还继承了IInterface接口 还必须实现其IBinder asBinder();方法。但是这个方法怎么实现返回我们却不知道。好在Stub实现除了我们自己定义接口方法之外IMyAidlInterface类的所有继承方法。

也就是说IMyAidlInterface我们定义了两个方法test1()和hello()没有被Stub实现,但是IMyAidlInterface其继承IInterface方法我们实现了。

所以我们写一个服务如下:

////MyService.java
package com.fmy.changevoice.aidl_resource;

public class MyService extends Service {



    @Override
    public IBinder onBind(Intent intent) {

        return new MyAidlInterface();
    }

    @Override
    public void onRebind(Intent intent) {
        super.onRebind(intent);
    }

    class MyAidlInterface extends IMyAidlInterface.Stub {



        @Override
        public void test1(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }

        @Override
        public String hello(String aString) throws RemoteException {
            return null;
        }
    }
}
  • 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
  • 分析2: 我们再看一下我们Stub类实例化的过程.在上面的Myservice中我们在实例化内部类MyAidlInterface 的时候会调用父类无参构造方法。所以我们从Stub的构造方法开始看。
 private static final String DESCRIPTOR = "com.fmy.changevoice.aidl_resource.IMyAidlInterface";

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
  • 1
  • 2
  • 3
  • 4
  • 5

构造方法调用attachInterface传入this和一个描述。而attachInterface属于Binder方法
继续跟进

 //将特定接口与Binder关联的便捷方法。
 //调用后,将为您实现queryLocalInterface()
 //返回给定的所有者IInterface时相应的
 //描述符被请求。
    public void attachInterface(IInterface owner, String descriptor) {
        mOwner = owner;
        mDescriptor = descriptor;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

DESCRIPTOR 字符串就是一个id一样 用于确定一个接口对应的Binder对象。而Binder对象是IBinder子类用于内存共享实现进程通信.当我们使用IInterface接口的asBinder()方法时候会根据descriptor返回对应IBinder对象

既然上文提到了queryLocalInterface ()那么我们顺便看看这个方法。
这个方法在Binder实现继承自IBinder

//使用提供给attachInterface()的信息来返回
关联的IInterface。
 public IInterface queryLocalInterface(String descriptor) {
        if (mDescriptor.equals(descriptor)) {
            return mOwner;
        }
        return null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

mOwner对象是Binder属性对象,就是我们attachInterface()传入的。回过头来再看看我们怎么调用attachInterface()的传入实参

 public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
  • 1
  • 2
  • 3

可见调用queryLocalInterface()方法后直接返回如果字符串匹配那么直接返回Stub对象

Stub父类的构造方法Binder如下

public Binder() {
        init();

        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Binder> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Binder class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
    }
  private native final void init();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

和可惜主要的init()方法是native方法,所以暂时到这


客户端

我们连接Myservice这个服务类代码如下

//MainActivity.java
package com.fmy.changevoice.aidl_resource;


public class MainActivity extends AppCompatActivity {

    private static final String TAG = "FMY";

    @Override
    protected void onStart() {
        super.onStart();
        Log.e(TAG, "onStart: ");
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //连接核心代码
        Intent intent = new Intent(this, MyService.class);
        bindService(intent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.e(TAG, "onServiceConnected: ");
               //32行 
                IMyAidlInterface.Stub.asInterface(service);

               try {
                    //调用接口中的一个方法. //36行
                    String test = iMyAidlInterface.hello("test");
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, BIND_AUTO_CREATE);
    }
}
  • 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

我们来先分析一下32行代码:
我们看看调用Stub的方法asInterface

     /**
        将IBinder对象转换为com .fmy. changevoice.aidl_resource.IMyAidlInterface接口,在需要时生成代理。
         */
        public static com.fmy.changevoice.aidl_resource.IMyAidlInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            //30行
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.fmy.changevoice.aidl_resource.IMyAidlInterface))) {
                return ((com.fmy.changevoice.aidl_resource.IMyAidlInterface) iin);
            }
            //34行
            return new com.fmy.changevoice.aidl_resource.IMyAidlInterface.Stub.Proxy(obj);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们先分析30行

//30行
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
  • 1
  • 2

queryLocalInterface方法在前面我们大致介绍了下,用你传入的DESCRIPTOR检索接口和binder相关的接口实现。如果你的ServiceClient在一个同一个进程那么直接返回本地对象,不需要进程间通信。否则返回空

如果返回本地对象那么后面也没有什么介绍了,我们假设这里ServiceClient不在同一进程。

34行

  return new com.fmy.changevoice.aidl_resource.IMyAidlInterface.Stub.Proxy(obj);
  • 1

可以看到假设不在同进程的时候用Proxy类创建返回IMyAidlInterface接口的实现

我们来大致看看Proxy类

这里写图片描述

ProxyStub静态私有内部类代码如下

   private static class Proxy implements com.fmy.changevoice.aidl_resource.IMyAidlInterface {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            /**
             * Demonstrates some basic types that you can use as parameters
             * and return values in AIDL.
             */
            @Override
            public void test1(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(anInt);
                    _data.writeLong(aLong);
                    _data.writeInt(((aBoolean) ? (1) : (0)));
                    _data.writeFloat(aFloat);
                    _data.writeDouble(aDouble);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_test1, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public String hello(String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_hello, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }
  • 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

我们先把精力放在Proxy构造方法上

  private static class Proxy implements com.fmy.changevoice.aidl_resource.IMyAidlInterface {
            private android.os.IBinder mRemote;
            //当Client和Service不在同进程的时候 传入Stub
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可见当ClientSerivice不在同进程的时候 ,Stub把自身传给Proxy构造对象 然后返回接口实例。
Stub因为继承自Binder可以把一些数据写入到共享内存中,所以保存此对象的一个引用。而proxy是我们接口的实现。

自此我们分析完成了
IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);这句代码

接下来我们看下用返回实例对象调用接口方法:

  Log.e(TAG, "onServiceConnected: ");
                //32
                IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);

                try {
                    //调用接口中的一个方法. //36行
                    String test = iMyAidlInterface.hello("test");
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 36行分析开始:
  String test = iMyAidlInterface.hello("test");
  • 1

我们知道aidl连接对象当客户端服务端不在同一进程的时候 ,在客户端返回的接口实例是Stub内部类Proxy的实例。

所以上面的调用 hell(“test”);的源码我们直接从Proxyhell()开始看

hell() 是我们最开始定义的两个方法的其中一个,另外一个是test1()

Proxy hell方法

这里写图片描述

上图已给出行号

先解释一个类android.os.Parcel 这个类运行在内存中传递序列化后的数据。Parcelable这类相比大家都用过。Parcelable内部就是封装了Parcel 对象而已。

Serializable是java提供的序列化,Parcel是google提供。更轻量并且在内存传输胜过Serializable

122-123获取parcel池获取对象实例,

   android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
  • 1
  • 2

_data 用于写入数据,reply用于接受数据。

124行接收结果。因为我们hello()返回值是String所以他自然也是String。

写入token。DESCRIPTOR就是我们前面说过确定Binder和哪个接口关联的一个ID。这里很明显。你要告诉我你要写个哪个Binder我才能确定 传个哪个接口。

 _data.writeInterfaceToken(DESCRIPTOR);
  • 1

127行

//写入参数
 _data.writeString(aString);
  • 1
  • 2

128行 放入Stub对象transact方法中 然后写入共享内存

mRemote.transact(Stub.TRANSACTION_hello, _data, _reply, 0);
  • 1

其中第一个参数 final的int值。是用于确定你调用哪个接口方法。每个定义接口方法都有一个对应的final值。其声明在Stub类中
本案例中
这里写图片描述

继续分析。。。。。。。
mRemote.transact()客户端的实现类是final class BinderProxy implements IBinder {。。。}
BinderProxyBinder 两个都是IBinder的实现类

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");
        if (Binder.isTracingEnabled()) { Binder.getTransactionTracker().addTrace(); }
        return transactNative(code, data, reply, flags);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

后面是JNI代码有兴趣的同学可以继续往下学习。
在这之后我们会回调到服务端的StubonTransact()方法

@Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_test1: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    long _arg1;
                    _arg1 = data.readLong();
                    boolean _arg2;
                    _arg2 = (0 != data.readInt());
                    float _arg3;
                    _arg3 = data.readFloat();
                    double _arg4;
                    _arg4 = data.readDouble();
                    java.lang.String _arg5;
                    _arg5 = data.readString();
                    this.test1(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_hello: {
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    java.lang.String _result = this.hello(_arg0);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
  • 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

上面的代码不需要介绍太多了相信代价都看得懂。

这里写图片描述
看完这些我们再来看看bindService的执行过程。这里我直接贴出时序图给大家看,不做过多介绍这里写图片描述

Activity的实现类为ContextImpl。你也许在继承类图看不到这个类的,但是你可以看到Activity的父类ContextWrapper的构造方法传入的一个对象实例是什么。
这里写图片描述

安卓7.1 新特性Shortcut

介绍

Shortcut 是谷歌在API25提出来的 类似苹果3D touch 但是没有压力感应.在安卓中完全就是长按.
来看下效果吧:
这里写图片描述

是不是很赞? 那么请随本文一起学习吧

更新

  1. 新建项目 在你项目下的build.gradle下

    以下目的很简单更新你编译工具 和指定项目版本

    1. compileSdkVersion 25
    2. buildToolsVersion “25.0.0”
    3. minSdkVersion 25
    4. targetSdkVersion 25
  2. 更新platform-tools 到25
    打开SDK Manager
    这里写图片描述
    如果你的Android SDK Platform-tools小于25那么请勾选然后点右下角更新

静态写法

静态写法?说白了和BroadcastReceiver(广播接受者)一样 .一个在清单文件中注册广播我们称为静态,用代码注册称为动态

  1. 在res创建xml文件夹
    这里写图片描述

  2. 在res/xml下新建一个文件命名为my_shortcut.xml字符串貌似必须引用方法比如@string/xxxx
    具体内容

    <shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
        <shortcut
            android:shortcutId="settings"
            android:enabled="true"
            android:icon="@mipmap/ic_launcher"
            android:shortcutShortLabel="@string/my_short"
            android:shortcutLongLabel="@string/my_long"
            android:shortcutDisabledMessage="@string/my_disable">
            <intent
                android:action="android.intent.action.VIEW"
                android:targetPackage="com.example.administrator.myapplication"
                android:targetClass="com.example.administrator.myapplication.MainActivity" />
            <intent
                android:action="android.intent.action.VIEW"
                android:targetPackage="com.example.administrator.myapplication"
                android:targetClass="com.example.administrator.myapplication.SettingsActivity" />
            <categories android:name="android.shortcut.conversation"/>
        </shortcut>
    </shortcuts>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    参数说明

    shortcut 属性说明:
    android:shortcutId 就是一个id标志 后面动态注册会讲到
    android:enabled 是否可用 如果不可用那么将不显示此快捷
    android:shortcutShortLabel 快捷短名:大家注意到一开始的效果图没?快捷是可以脱出来在变成一个桌面快捷方式图标.那么此图标的名字就是这个
    android:shortcutLongLabel :快捷长名 长按下图标弹出来列表框中每个快捷名
    android:shortcutDisabledMessage: 当快捷不可用时用户点击会提示此文字 后面动态会详细说明
    intent属性说明:

    假设1:shortcut (看清楚不是shorcuts 没有s哦)下只有一个intent 那么结果:用户点击此快捷用户跳转到intent制定的activity

    假设2:shortcut 下有两个intent 我们按照顺序命名为intent1intent2 那么用户点击快捷的时候将会跳转到intent2 此时 若用户按下back键(返回键) 那么将会跳转到intent1的界面

    categories 属性说明
    反正就一个值就是上面写的 写死即可

  3. 在清单文件注册
    注意一个小坑:注册信息必须要在activity为启动项的activity的根标签注册写下<meta-data>

     <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
                <meta-data
                    android:name="android.app.shortcuts"
                    android:resource="@xml/my_shortcut"/>
            </activity>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    做法如下:
    下面少打错了”android.app.shortcuts” 下面少打了个s (电脑实在太卡了,不想重录)注意!!!!!!!!
    这里写图片描述

  4. 效果展示:
    这里写图片描述

  5. 小知识点
    假如:你打开快捷item的程序所在的应用已经有多个activity在回退栈 请猜猜会怎么样?这里留给读者自行尝试..哪怕什么都没有反生你也可以增加记忆嘛

动态写法 -添加

特点和广播接受者一样灵活
核心代码(本例只要点击”创建”按钮会执行下面方法生成快捷):

 //动态添加
    public void onclick2(View view) {
        mShortcutManager = getSystemService(ShortcutManager.class);
        List<ShortcutInfo> infos = new ArrayList<>();
        //快捷最多只能有5个
        // getMaxShortcutCountPerActivity只能返回5
        for (int i = 0; i < mShortcutManager.getMaxShortcutCountPerActivity(); i++) {
            Intent intent = new Intent(this, SettingsActivity.class);
            intent.setAction(Intent.ACTION_VIEW);
            Intent intent2 = new Intent();
            intent2.setAction("fmy.fmy");
            intent2.setClassName(getPackageName(),getPackageName()+".MainActivity.java");
            Intent[] intents = new Intent[2];
            //开始点击快捷时跳进此 返回键跳入intent2 其他类似
            intents[0]=intent;
            intents[1]=intent2;
                //第一个参数 上下文
                //第二个参数id嘛            
            ShortcutInfo info = new ShortcutInfo.Builder(this, "id" + i)
                    .setShortLabel("短的名字"+i+"")
                    .setLongLabel("长的名字:" + i+"")
                    .setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher))
//                   .setIntent(intent)
                    .setIntents(intents)
                    .build();
            infos.add(info);

        }

        mShortcutManager.setDynamicShortcuts(infos);
    }
  • 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

效果:

解析:下图我一开始没有点击”创建”按钮 直接在桌面长按按钮发现没有任何反应.然后进入程序按下创建按钮并返回桌面发现可以长按点出快捷
这里写图片描述

注意和静态写法一起的坑(算本人经验吧):

那些年我们一起踩过的坑—>>上面的代码会动态创建5个快捷点击item.但是如果你此时静态写一个了快捷item那么恭喜你见红了(出现异常)

Caused by: java.lang.IllegalArgumentException: Max number of dynamic shortcuts exceeded

解决:先获取其已有的快捷item数量然后要么移除原来的,要么减少你创建.或者更新 这就是我为什么知道只能创建5个原因.解决方法读者看完 “动态写法-更新(覆盖)”和”动态写法-删除”自然会明白,如果想先解决问题那么请直接拷贝 更新 和 删除 中部分核心代码

动态写法 -更新(覆盖)

如果你想某些时候改变某些快捷item的名字或者意图(intent)那么请参照以下代码

 public void onclick3(View view) {
        Intent intent2 = new Intent();
        intent2.setAction("fmy.fmy");
        intent2.setClassName(getPackageName(),getPackageName()+".MainActivity.java");
//设置id为id1 会覆盖原来快捷item为id为id1的快捷
//如果没有则什么都不会发生
        ShortcutInfo info = new ShortcutInfo.Builder(this,"id1")
                .setIntent(intent2)
                .setLongLabel("动态更新的长名")
                .setShortLabel("动态更新的短名")
                .build();
        mShortcutManager = getSystemService(ShortcutManager.class);
        List<ShortcutInfo> dynamicShortcuts = mShortcutManager.getDynamicShortcuts();
        mShortcutManager.updateShortcuts(Arrays.asList(info));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

效果:
这里写图片描述

动态写法 -删除(不可用)

下面一小段描述转载的(我不想写,再说次作者写这个描述非常不错此段描述原作者地址)
我们先来介绍一个名词-Pinning Shortcuts, 这是个啥玩意呢? 其实对于Shortcut, Android是允许我们直接放到桌面的, 这样就更加方便了用户的操作, google把他称作为Pinning Shortcuts, 具体啥样, 我们来张图就明白了.
这里写图片描述
对于这个Pinning Shortcuts, google的文档说, 我们开发者是没有权利去删除的, 能删除它的只有用户. 那我该项功能删除了咋办? 这东西还在桌面上, 是不是APP要崩? 当然Google考虑到了这点, 它允许我们去disable这个shortcut. 让其变为灰色 用户点击时提示个小土司
好了引用结束 感谢原作者

我们在桌面长按拖出来的快捷item到桌面 这个item对象为ShortcutInfo
代码是最好的老师:

//删除
    public void onclick(View view) {

        mShortcutManager = getSystemService(ShortcutManager.class);
        //获取所有被拉取出来的快捷item(如果一个item都没有被拉出那么返回长度为0)
        List<ShortcutInfo> infos = mShortcutManager.getPinnedShortcuts();
        //遍历所有的被拉出的item 然后让其变成灰色不可点击
        for (ShortcutInfo info :
                infos ) {
            //此时被拉出的快捷item 变为灰色 用户再点击 会弹出吐司内容为第二个参数 "不可点击哦"
           // 此时桌面长按原程序图标弹出的快捷列表已经没有了
            mShortcutManager.disableShortcuts(Arrays.asList(info.getId()), "不可点击哦");
            List<ShortcutInfo> dynamicShortcuts = mShortcutManager.getDynamicShortcuts();
            Log.e("TAG","大小"+dynamicShortcuts.size());
            //虽然不可见但是你 依然要移除在动态添加列表里的东西 不过我调用disableShortcuts
            // 后发现其大小变了.内部应该调用此方法了.由于电脑太卡没下载源码 所以保险起见写上吧
            mShortcutManager.removeDynamicShortcuts(Arrays.asList(info.getId()));

        }


        List<ShortcutInfo> dynamicShortcuts = mShortcutManager.getDynamicShortcuts();
        Log.e("TAG","大小"+dynamicShortcuts.size());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

小知识点

  1. 用户删除数据时 被拖出来快捷item会被删除
  2. 用户删除数据时 动态创建的item 你在桌面在长按程序图标也没有 需要重新写入
  3. 用户卸载时被拖出来快捷item会被删除

关于这篇博文

我偶然看到这个7.1新特性 于是一直在找学习资料.然后想写下 期间看了几篇文章 并结合自己体会写下来.这篇博客用到模拟器要用SDK manager 下载镜像 因为只有它有7.1镜像 genymotion 最新的也就只有7.0 而已.运行谷歌自带镜像及其耗费内存 我就4G内存 开完stuio和博客和模拟器 内存只剩下80mb 卡的程度可想而知.但是一直想写一篇高质量的博文.于是硬着头皮卡了5个小时写下了.由于时间仓促错漏在所难免,由于卡到不行不敢点击源码去看 而且我也没下载.如果以上文字对你有那么一点带你帮助 将是我最大的欣慰;

自定义view实现水波纹效果

今天看到一篇自定view 实现水波纹效果 觉得真心不错 学习之后再次写下笔记和心得.但是感觉原作者写得有些晦涩难懂,也许是本人愚笨 所以重写此作者教程.原作者博文大家可以去看下,感觉他在自定义view方面非常厉害,本文是基于此作者原文重新改写,拥有大量像相似部分

先看下效果吧:
1. 效果1:
这里写图片描述
2. 效果2
这里写图片描述


我先们来学习效果1:

效果1实现本质:用一张波形图和一个圆形图的图片,然后圆形图波形图上方,然后使用安卓的图片遮罩模式desIn(不懂?那么先记住有这样一个遮罩模式).(只显示上部图像和下部图像公共部分的下半部分),是不是很难懂?那么我在说清一点并且配图.假设圆形图波形图上面,那么只会显示两者相交部分的波形图
下面是解释效果图(正方形蓝色图片在黄色圆形上面):
这里写图片描述

学习此模式具体地址学习安卓图片遮罩模式

这里写图片描述

所用到波形图:
这里写图片描述

所用到圆形图:

这里写图片描述

这次的实现我们都选择继承view,在实现的过程中我们需要关注如下几个方法:

1.onMeasure():最先回调,用于控件的测量;

2.onSizeChanged():在onMeasure后面回调,可以拿到view的宽高等数据,在横竖屏切换时也会回调;

3.onDraw():真正的绘制部分,绘制的代码都写到这里面;

先来看看我们定义的变量:


    //波形图
    Bitmap waveBitmap;

    //圆形遮罩图
    Bitmap circleBitmap;

    //波形图src
    Rect waveSrcRect;
    //波形图dst
    Rect waveDstRect;

    //圆形遮罩src
    Rect circleSrcRect;

    //圆形遮罩dst
    Rect circleDstRect;

    //画笔
    Paint mpaint;

    //图片遮罩模式
    PorterDuffXfermode mode;

    //控件的宽
    int viewWidth;
    //控件的高
    int viewHeight;

    //图片过滤器
    PaintFlagsDrawFilter paintFlagsDrawFilter ;

    //每次移动的距离
    int speek = 10 ;

    //当前移动距离
    int nowOffSet;
  • 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

介绍一个方法:

 void android.graphics.Canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
  • 1

此方法的参数:

参数1:你的图片

参数2:矩形 .也就是说此矩形决定你画出图片参数1 的哪个位置,比如说你的矩形是设定是Rect rect= new Rect(0,0,图片宽,图片高) 那么将会画出图片全部

参数3:矩形.决定你图片缩放比例和在view中的位置.假设你的矩形Rect rect= new Rect(0,0,100,100) 那么你将在自定义view中(0,0)点到(100,100)绘画此图片并且如果图片大于(小于)此矩形那么按比例缩小(放大)

来看看 初始化方法

//初始化
    private void init() {

        mpaint = new Paint();
        //处理图片抖动
        mpaint.setDither(true);
        //抗锯齿
        mpaint.setAntiAlias(true);
        //设置图片过滤波
        mpaint.setFilterBitmap(true);
        //设置图片遮罩模式
        mode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

        //给画布直接设定参数
        paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.DITHER_FLAG|Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG);

        //初始化图片
        //使用drawable获取的方式,全局只会生成一份,并且系统会进行管理,
        //而BitmapFactory.decode()出来的则decode多少次生成多少张,务必自己进行recycle;

        //获取波形图
        waveBitmap = ((BitmapDrawable)getResources().getDrawable(R.drawable.wave_2000)).getBitmap();

        //获取圆形遮罩图
        circleBitmap = ((BitmapDrawable)getResources().getDrawable(R.drawable.circle_500)).getBitmap();

        //不断刷新波形图距离 读者可以先不看这部分内容  因为需要结合其他方法
        new Thread(){
            public void run() {
                while (true) {
                    try {
                        //移动波形图
                        nowOffSet=nowOffSet+speek;
                        //如果移动波形图的末尾那么重新来
                        if (nowOffSet>=waveBitmap.getWidth()) {
                            nowOffSet=0;
                        }
                        sleep(30);
                        postInvalidate();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }

            };
        }.start();

    }
  • 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

以下获取view的宽高并设置对应的波形图和圆形图矩形(会在onMesure回调后执行)

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //获取view宽高
        viewWidth = w;
        viewHeight = h ;

        //波形图的矩阵初始化
        waveSrcRect = new Rect();
        waveDstRect = new Rect(0,0,w,h);

        //圆球矩阵初始化
        circleSrcRect = new Rect(0,0,circleBitmap.getWidth(),circleBitmap.getHeight());

        circleDstRect = new Rect(0,0,viewWidth,viewHeight);


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

那么最后来看看绘画部分吧

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);




        //给图片直接设置过滤效果
        canvas.setDrawFilter(paintFlagsDrawFilter);
        //给图片上色
        canvas.drawColor(Color.TRANSPARENT);
        //添加图层 注意!!!!!使用图片遮罩模式会影响全部此图层(也就是说在canvas.restoreToCount 所有图都会受到影响) 
        int saveLayer = canvas.saveLayer(0,0, viewWidth,viewHeight,null, Canvas.ALL_SAVE_FLAG);
        //画波形图部分 矩形
        waveSrcRect.set(nowOffSet, 0, nowOffSet+viewWidth/2, viewHeight);
        //画矩形
        canvas.drawBitmap(waveBitmap,waveSrcRect,waveDstRect,mpaint);
        //设置图片遮罩模式
        mpaint.setXfermode(mode);
        //画遮罩
        canvas.drawBitmap(circleBitmap, circleSrcRect, circleDstRect,mpaint);
        //还原画笔模式
        mpaint.setXfermode(null);
        //将图层放上
        canvas.restoreToCount(saveLayer);
    }
  • 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

最后看下完整的代码

package com.fmy.shuibo1;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.icu.text.TimeZoneFormat.ParseOption;
import android.util.AttributeSet;
import android.view.View;

public class MySinUi extends View{


    //波形图
    Bitmap waveBitmap;

    //圆形遮罩图
    Bitmap circleBitmap;

    //波形图src
    Rect waveSrcRect;
    //波形图dst
    Rect waveDstRect;

    //圆形遮罩src
    Rect circleSrcRect;

    //圆形遮罩dst
    Rect circleDstRect;

    //画笔
    Paint mpaint;

    //图片遮罩模式
    PorterDuffXfermode mode;

    //控件的宽
    int viewWidth;
    //控件的高
    int viewHeight;

    //图片过滤器
    PaintFlagsDrawFilter paintFlagsDrawFilter ;

    //每次移动的距离
    int speek = 10 ;

    //当前移动距离
    int nowOffSet;

    public MySinUi(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();

    }

    //初始化
    private void init() {

        mpaint = new Paint();
        //处理图片抖动
        mpaint.setDither(true);
        //抗锯齿
        mpaint.setAntiAlias(true);
        //设置图片过滤波
        mpaint.setFilterBitmap(true);
        //设置图片遮罩模式
        mode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

        //给画布直接设定参数
        paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.DITHER_FLAG|Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG);

        //初始化图片
        //使用drawable获取的方式,全局只会生成一份,并且系统会进行管理,
        //而BitmapFactory.decode()出来的则decode多少次生成多少张,务必自己进行recycle;

        //获取波形图
        waveBitmap = ((BitmapDrawable)getResources().getDrawable(R.drawable.wave_2000)).getBitmap();

        //获取圆形遮罩图
        circleBitmap = ((BitmapDrawable)getResources().getDrawable(R.drawable.circle_500)).getBitmap();

        //不断刷新波形图距离 读者可以先不看这部分内容  因为需要结合其他方法
        new Thread(){
            public void run() {
                while (true) {
                    try {
                        //移动波形图
                        nowOffSet=nowOffSet+speek;
                        //如果移动波形图的末尾那么重新来
                        if (nowOffSet>=waveBitmap.getWidth()) {
                            nowOffSet=0;
                        }
                        sleep(30);
                        postInvalidate();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }

            };
        }.start();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);




        //给图片直接设置过滤效果
        canvas.setDrawFilter(paintFlagsDrawFilter);
        //给图片上色
        canvas.drawColor(Color.TRANSPARENT);
        //添加图层 注意!!!!!使用图片遮罩模式会影响全部此图层(也就是说在canvas.restoreToCount 所有图都会受到影响) 
        int saveLayer = canvas.saveLayer(0,0, viewWidth,viewHeight,null, Canvas.ALL_SAVE_FLAG);
        //画波形图部分 矩形
        waveSrcRect.set(nowOffSet, 0, nowOffSet+viewWidth/2, viewHeight);
        //画矩形
        canvas.drawBitmap(waveBitmap,waveSrcRect,waveDstRect,mpaint);
        //设置图片遮罩模式
        mpaint.setXfermode(mode);
        //画遮罩
        canvas.drawBitmap(circleBitmap, circleSrcRect, circleDstRect,mpaint);
        //还原画笔模式
        mpaint.setXfermode(null);
        //将图层放上
        canvas.restoreToCount(saveLayer);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //获取view宽高
        viewWidth = w;
        viewHeight = h ;

        //波形图的矩阵初始化
        waveSrcRect = new Rect();
        waveDstRect = new Rect(0,0,w,h);

        //圆球矩阵初始化
        circleSrcRect = new Rect(0,0,circleBitmap.getWidth(),circleBitmap.getHeight());

        circleDstRect = new Rect(0,0,viewWidth,viewHeight);


    }
}
  • 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
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159

学习效果2:

此方法实现原理:运用三角函数画出两个不同速率正弦函数图

我们先来复习三角函数吧

正余弦函数方程为:
y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;

w:周期就是一个完整正弦曲线图此数值越大sin的周期越小 (cos越大)
如下图:
这里写图片描述
(原作者说我们画一个以自定义view的宽度为周期的图:意思是说你view的宽度正好可以画一个上面的图.)

这里写图片描述

A:振幅两个山峰最大的高度.如果A越大两个山峰越高和越低

h:你正弦曲线和y轴相交点.(影响正弦图初始高度的位置)

b:初相会让你图片向x轴平移

具体大家可以百度学习,我们在学编程,不是数学


为什么要两个正弦图画?好看…..
先来看看变量:

// 波纹颜色
    private static final int WAVE_PAINT_COLOR = 0x880000aa;

    // 第一个波纹移动的速度
    private int oneSeep = 7;

    // 第二个波纹移动的速度
    private int twoSeep = 10;

    // 第一个波纹移动速度的像素值
    private int oneSeepPxil;
    // 第二个波纹移动速度的像素值
    private int twoSeepPxil;

    // 存放原始波纹的每个y坐标点
    private float wave[];

    // 存放第一个波纹的每一个y坐标点
    private float oneWave[];

    // 存放第二个波纹的每一个y坐标点
    private float twoWave[];

    // 第一个波纹当前移动的距离
    private int oneNowOffSet;
    // 第二个波纹当前移动的
    private int twoNowOffSet;

    // 振幅高度
    private int amplitude = 20;

    // 画笔
    private Paint mPaint;

    // 创建画布过滤
    private DrawFilter mDrawFilter;

    // view的宽度
    private int viewWidth;

    // view高度
    private int viewHeight;
  • 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

画初始的波形图并且保存到数组中

// 大小改变
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 获取view的宽高
        viewHeight = h;
        viewWidth = w;

        // 初始化保存波形图的数组
        wave = new float[w];
        oneWave = new float[w];
        twoWave = new float[w];

        // 设置波形图周期
        float zq = (float) (Math.PI * 2 / w);

        // 设置波形图的周期
        for (int i = 0; i < viewWidth; i++) {
            wave[i] = (float) (amplitude * Math.sin(zq * i));
        }


    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

初始化各种

// 初始化
    private void init() {
        // 创建画笔
        mPaint = new Paint();
        // 设置画笔颜色
        mPaint.setColor(WAVE_PAINT_COLOR);
        // 设置绘画风格为实线
        mPaint.setStyle(Style.FILL);
        // 抗锯齿
        mPaint.setAntiAlias(true);
        // 设置图片过滤波和抗锯齿
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);

        // 第一个波的像素移动值 换算成手机像素值让其在各个手机移动速度差不多
        oneSeepPxil = dpChangPx(oneSeep);

        // 第二个波的像素移动值
        twoSeepPxil = dpChangPx(twoSeep);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
// 绘画方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.setDrawFilter(mDrawFilter);

        oneNowOffSet =oneNowOffSet+oneSeepPxil;

        twoNowOffSet = twoNowOffSet+twoSeepPxil;

        if (oneNowOffSet>=viewWidth) {
            oneNowOffSet = 0;
        }
        if (twoNowOffSet>=viewWidth) {
            twoNowOffSet = 0;
        }
        //此方法会让两个保存波形图的 数组更新 头到NowOffSet变成尾部,尾部的变成头部实现动态移动
        reSet();

        Log.e("fmy", Arrays.toString(twoWave));

        for (int i = 0; i < viewWidth; i++) {

            canvas.drawLine(i, viewHeight, i, viewHeight-400-oneWave[i], mPaint);
            canvas.drawLine(i, viewHeight, i, viewHeight-400-twoWave[i], mPaint);
        }

        postInvalidate();
    }
  • 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

来看看能让两个数组重置的


    public void reSet() {
        // one是指 走到此处的波纹的位置 (这个理解方法看个人了)
        int one = viewWidth - oneNowOffSet;
        // 把未走过的波纹放到最前面 进行重新拼接
        System.arraycopy(wave, oneNowOffSet, oneWave, 0, one);
        // 把已走波纹放到最后
        System.arraycopy(wave, 0, oneWave, one, oneNowOffSet);

        // one是指 走到此处的波纹的位置 (这个理解方法看个人了)
        int two = viewWidth - twoNowOffSet;
        // 把未走过的波纹放到最前面 进行重新拼接
        System.arraycopy(wave, twoNowOffSet, twoWave, 0, two);
        // 把已走波纹放到最后
        System.arraycopy(wave, 0, twoWave, two, twoNowOffSet);


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

最后大家看下完整代码

package com.exam1ple.myshuibo2;

import java.util.Arrays;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DrawFilter;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.icu.text.TimeZoneFormat.ParseOption;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;

public class MyUi2 extends View {

    // 波纹颜色
    private static final int WAVE_PAINT_COLOR = 0x880000aa;

    // 第一个波纹移动的速度
    private int oneSeep = 7;

    // 第二个波纹移动的速度
    private int twoSeep = 10;

    // 第一个波纹移动速度的像素值
    private int oneSeepPxil;
    // 第二个波纹移动速度的像素值
    private int twoSeepPxil;

    // 存放原始波纹的每个y坐标点
    private float wave[];

    // 存放第一个波纹的每一个y坐标点
    private float oneWave[];

    // 存放第二个波纹的每一个y坐标点
    private float twoWave[];

    // 第一个波纹当前移动的距离
    private int oneNowOffSet;
    // 第二个波纹当前移动的
    private int twoNowOffSet;

    // 振幅高度
    private int amplitude = 20;

    // 画笔
    private Paint mPaint;

    // 创建画布过滤
    private DrawFilter mDrawFilter;

    // view的宽度
    private int viewWidth;

    // view高度
    private int viewHeight;

    // xml布局构造方法
    public MyUi2(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    // 初始化
    private void init() {
        // 创建画笔
        mPaint = new Paint();
        // 设置画笔颜色
        mPaint.setColor(WAVE_PAINT_COLOR);
        // 设置绘画风格为实线
        mPaint.setStyle(Style.FILL);
        // 抗锯齿
        mPaint.setAntiAlias(true);
        // 设置图片过滤波和抗锯齿
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);

        // 第一个波的像素移动值 换算成手机像素值让其在各个手机移动速度差不多
        oneSeepPxil = dpChangPx(oneSeep);

        // 第二个波的像素移动值
        twoSeepPxil = dpChangPx(twoSeep);
    }

    // 绘画方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.setDrawFilter(mDrawFilter);

        oneNowOffSet =oneNowOffSet+oneSeepPxil;

        twoNowOffSet = twoNowOffSet+twoSeepPxil;

        if (oneNowOffSet>=viewWidth) {
            oneNowOffSet = 0;
        }
        if (twoNowOffSet>=viewWidth) {
            twoNowOffSet = 0;
        }
        reSet();

        Log.e("fmy", Arrays.toString(twoWave));

        for (int i = 0; i < viewWidth; i++) {

            canvas.drawLine(i, viewHeight, i, viewHeight-400-oneWave[i], mPaint);
            canvas.drawLine(i, viewHeight, i, viewHeight-400-twoWave[i], mPaint);
        }

        postInvalidate();
    }

    public void reSet() {
        // one是指 走到此处的波纹的位置 (这个理解方法看个人了)
        int one = viewWidth - oneNowOffSet;
        // 把未走过的波纹放到最前面 进行重新拼接
        System.arraycopy(wave, oneNowOffSet, oneWave, 0, one);
        // 把已走波纹放到最后
        System.arraycopy(wave, 0, oneWave, one, oneNowOffSet);

        // one是指 走到此处的波纹的位置 (这个理解方法看个人了)
        int two = viewWidth - twoNowOffSet;
        // 把未走过的波纹放到最前面 进行重新拼接
        System.arraycopy(wave, twoNowOffSet, twoWave, 0, two);
        // 把已走波纹放到最后
        System.arraycopy(wave, 0, twoWave, two, twoNowOffSet);


    }

    // 大小改变
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 获取view的宽高
        viewHeight = h;
        viewWidth = w;

        // 初始化保存波形图的数组
        wave = new float[w];
        oneWave = new float[w];
        twoWave = new float[w];

        // 设置波形图周期
        float zq = (float) (Math.PI * 2 / w);

        // 设置波形图的周期
        for (int i = 0; i < viewWidth; i++) {
            wave[i] = (float) (amplitude * Math.sin(zq * i));
        }


    }

    // dp换算成px 为了让移动速度在各个分辨率的手机的都差不多
    public int dpChangPx(int dp) {
        DisplayMetrics metrics = new DisplayMetrics();
        ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
        return (int) (metrics.density * dp + 0.5f);
    }

}
  • 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
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174

以上源代码:`
源码奉上各位

自定义view实现阻尼效果的加载动画

效果:
这里写图片描述

>

需要知识:
1. 二次贝塞尔曲线
2. 动画知识
3. 基础自定义view知识

先来解释下什么叫阻尼运动

阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又称减幅振动、衰减振动。[1] 不论是弹簧振子还是单摆由于外界的摩擦和介质阻力总是存在,在振动过程中要不断克服外界阻力做功,消耗能量,振幅就会逐渐减小,经过一段时间,振动就会完全停下来。这种振幅随时间减小的振动称为阻尼振动.因为振幅与振动的能量有关,阻尼振动也就是能量不断减少的振动.阻尼振动是非简谐运动.阻尼振动系统属于耗散系统。这里的阻尼是指任何振动系统在振动中,由于外界作用或系统本身固有的原因引起的振动幅度逐渐下降的特性,以及此一特性的量化表征。
这里写图片描述

本例中文字部分凹陷就是这种效果,当然这篇文章知识带你简单的使用.

跳动的水果效果实现

剖析:从上面的效果图中很面就是从顶端向下掉落然后再向上 期间旋转即可.
那么我们首先自定义一个view继承FrameLayout

public class My extends FrameLayout {

    public My(Context context) {
        super(context);
    }

    public My(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

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

需要素材如下三张图片:

这里写图片描述
这里写图片描述
这里写图片描述

也许有人会问我看到你效果图到顶部或者底部就变成向上或者向下了.你三张够吗?
答:到顶部或者底部旋转180度即可
我们现在自定义中定义几个变量


    //用于记录当前图片使用数组中的哪张
    int indexImgFlag = 0;

    //下沉图片 前面三个图片的id
    int allImgDown [] = {R.mipmap.p2,R.mipmap.p4,R.mipmap.p6,R.mipmap.p8};

    //动画效果一次下沉或上弹的时间 animationDuration*2=一次完整动画时间
    int animationDuration = 1000;

    //弹起来的图片
    ImageView iv;

    //图片下沉高度(即从最高点到最低点的距离)
    int downHeight = 2;
    //掉下去的动画
    private Animation translateDown;
    //弹起动画
    private Animation translateUp;
    //旋转动画
    private ObjectAnimator rotation;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

我们再来看看初始化动画的方法(此方法使用了递归思想,实现无限播放动画,大家可以看看哪里不理解)

 //初始化弹跳动画
    public void  MyAnmation(){
        //下沉效果动画
        translateDown = new TranslateAnimation(
                Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0,
                Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,downHeight);
        translateDown.setDuration(animationDuration);
        //设置一个插值器 动画将会播放越来越快 模拟重力
        translateDown.setInterpolator(new AccelerateInterpolator());


        //上弹动画
        translateUp = new TranslateAnimation(
         Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0,
          Animation.RELATIVE_TO_SELF,downHeight,Animation.RELATIVE_TO_SELF,0
       );

        translateUp.setDuration(animationDuration);
        ////设置一个插值器 动画将会播放越来越慢 模拟反重力
        translateUp.setInterpolator(new DecelerateInterpolator());


        //当下沉动画完成时播放启动上弹
        translateDown.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                iv.setImageResource(allImgDown[indexImgFlag]);
                rotation = ObjectAnimator.ofFloat(iv, "rotation", 180f, 360f);
                rotation.setDuration(1000);
                rotation.start();
            }


            @Override
            public void onAnimationEnd(Animation animation) {

                iv.startAnimation(translateUp);


            }


            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });

        //当上移动画完成时 播放下移动画
        translateUp.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                indexImgFlag = 1+indexImgFlag>=allImgDown.length?0:1+indexImgFlag;
                iv.setImageResource(allImgDown[indexImgFlag]);
                rotation = ObjectAnimator.ofFloat(iv, "rotation", 0.0f, 180f);
                rotation.setDuration(1000);

                rotation.start();
            }


            @Override
            public void onAnimationEnd(Animation animation) {
                //递归
                iv.startAnimation(translateDown);

            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }
  • 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

以上代码知识:

插值器:会让一个动画在播放时在某一时间段加快动画或者减慢

//设置一个插值器 动画将会播放越来越快 模拟重力
1. translateDown.setInterpolator(new AccelerateInterpolator());
这个插值器 速率表示图:
这里写图片描述

可以从斜率看到使用此插值器 动画将越来越快.意义在于模仿下落时重力的影响

////设置一个插值器 动画将会播放越来越慢 模拟反重力
2. translateUp.setInterpolator(new DecelerateInterpolator());
速率图:
这里写图片描述

最后我们初始化下图片控件到我们的自定义view

private void init() {

    //初始化弹跳图片 控件
    iv = new ImageView(getContext());

    ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT);

    iv.setLayoutParams(params);
    iv.setImageResource(allImgDown[0]);


    this.addView(iv);

    iv.measure(0,0);

    iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {

            if (!flagMeure)
            {
                flagMeure =true;
                //由于画文字是由基准线开始
                path.moveTo(iv.getX()-textWidth/2+iv.getWidth()/2, textHeight+iv.getHeight()+downHeight*iv.getHeight());

                //计算最大弹力
                maxElasticFactor = (float) (textHeight / elastic);
                //初始化贝塞尔曲线
                path.rQuadTo(textWidth / 2, 0, textWidth, 0);

                //初始化上弹和下沉动画
                MyAnmation();

                iv.startAnimation(translateDown);
            }
        }
    });
  • 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

}

上面的知识:
1. iv.measure(0,0);主动通知系统去测量此控件 不然iv.getwidth = 0;
//下面这个是同理 等iv测量完时回调 不然iv.getwidth = 0;
2. iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener(){

}

原因:TextView tv = new TextView() 或者 LayoutInflat 填充布局都是
异步所以你在new出来或者填充时直接获取自然返回0

到现在为止你只需要在自定义view 的onSizeChanged回调方法中调用init()即可看到动画的弹动

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        init();
    }
  • 1
  • 2
  • 3
  • 4
  • 5

此方法会在onmesure方法执行完成后回调 这样你就可以在此方法获得自定义view的宽高了

效果图:
这里写图片描述


画文字成u形

首先你得知道如下知识

贝塞尔曲线:具体学习

这里我们用到2此贝塞尔曲线
我们看看大概是什么叫2次贝塞尔曲线

这里写图片描述
我们看看 三个点 p0 p1 p2 我们 把p0 称为 开始点 p1 为控制点 p2 结束点,那么可以用贝塞尔公式画出如图曲线

这里写图片描述

这里大家没必要深究怎么画出来. 也不需要你懂 这个要数学基础的

那么我们在安卓中怎么画呢?

 Path path = new Path();
//p0的x y坐标
path.moveTo(p0.x,y);
path.rQuadTo(p1.x,p1.y,p2.x,p2.y);
  • 1
  • 2
  • 3
  • 4

这就是API调用方法是不是很简单?那么你又会问那么怎么画出来呢?
很简单在 dispatchDraw方法 或者onDraw中 调用

  @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.drawPath(path,paint);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

那么你画出来的效果应该和在Ps用钢笔画出来的差不多 ps中钢笔工具就是二次贝塞尔曲线
这里写图片描述
(借用下图片)

如果你的三个点的位置如刚开的图片 p0 p1 p2 (p1在p0右上方并且 p1在p2左上方)一样那么在屏幕中的显示效果如下
这里写图片描述
这里随扩张下dispatchDraw和ondraw的区别
如果你的自定义view 是继承view 那么 会先调用 ondraw->>dispatchDraw
如果你的自定义view 是继承viewgroup那么会跳过ondraw方法直接调用dispathchDraw这里特别注意!!我们这个案例中继承的是FrameLayout,
而frameLayout又是继承自viewgroup所以….

那么我们回到主题 如何画一个U形文字?简单就是说按照我们画出来的曲线在上面写字 如: 文字是”CSDN开源中国” 如何让这几个字贴着我们的曲线写出来?
这里介绍一个API
canvas.drawTextOnPath()
这里写图片描述
第一个参数:文字 类型为字符串
第二个参数:路径 也就是我们前面的二次贝塞尔曲线
第三个参数:沿着路径文字开始的位置 说白了偏移量
第四个参数:贴着路径的高度的偏移量

hOffset:
The distance along the path to add to the text’s starting position
vOffset:
The distance above(-) or below(+) the path to position the text

//ok我们看看他可以画出什么样子的文字
这里写图片描述

这种看大家对贝塞尔曲线的理解,你理解的越深那么你可以画出的图像越多,当然不一定要用贝塞尔曲线


确定贝塞尔曲线的起点

我们在回过头来看看我们的效果图

这里写图片描述

我们可以看到文字应该是在iv(弹跳的图片中央位置且正好在 iv弹到底部的位置)

这里我们先补充知识在考虑计算

我们来学习一下文字的测量我们来看幅图
这里写图片描述

我们调用画文字的API时
canvas.drawTextOnPath或者canvas.drawText 是从基准线开始画的也就是说途中的baseline开始画.
如:
canvas.drawText(“FMY”,0,0,paint);
那么你将看不到文字 只能在屏幕看到文字底部如下图:
这里写图片描述

另一个同理API drawTextOnPath 也是

再看看几个简单的API
1 . paint.measureText(“FMY”);返回在此画笔paint下写FMY文字的宽度
下面的API会把文字的距离左边left 上边top 右边right 底部的bottom的值写入此矩形 那么
rect.right-rect.left=文字宽度
rect.bottom-rect.top=文字高度

  //矩形
 Rect   rect = new Rect();
 //将文字画入矩形目的是为了测量高度
 paint.getTextBounds(printText, 0, printText.length(), rect);
  • 1
  • 2
  • 3
  • 4
  • 5

那么请看:

 private void init() {

        //初始化弹跳图片 控件
        iv = new ImageView(getContext());

        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);

        iv.setLayoutParams(params);
        iv.setImageResource(allImgDown[0]);


        this.addView(iv);

        //画笔的初始化
        paint = new Paint();
        paint.setStrokeWidth(1);
        paint.setColor(Color.CYAN);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(50);
        paint.setAntiAlias(true);

        //矩形
     Rect   rect = new Rect();
        //将文字画入矩形目的是为了测量高度
        paint.getTextBounds(printText, 0, printText.length(), rect);

        //文本宽度
        textWidth = paint.measureText(printText);

        //获得文字高度
        textHeight = rect.bottom - rect.top;

        //初始化路径
        path = new Path();

        iv.setX(getWidth()/2);

        iv.measure(0,0);

        iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {

                if (!flagMeure)
                {
                    flagMeure =true;



                    //由于画文字是由基准线开始
                    path.moveTo(iv.getX()-textWidth/2+iv.getWidth()/2, textHeight+iv.getHeight()+downHeight*iv.getHeight());

                    //计算最大弹力
                    maxElasticFactor = (float) (textHeight / elastic);
                    //初始化贝塞尔曲线
                    path.rQuadTo(textWidth / 2, 0, textWidth, 0);

                    //初始化上弹和下沉动画
                    MyAnmation();

                    iv.startAnimation(translateDown);
                }
            }
        });




    }
  • 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

我们现在写一个类当iv图片(弹跳图)碰到文字顶部时设置一个监听器 时间正好是弹图向上到顶部的时间 期间不断让文字凹陷在恢复正常

  //用于播放文字下沉和上浮动画传入的数值必须是 图片下沉和上升的一次时间
    public void initAnimation(int duration){
        //这里为什maxElasticFactor/4 好看...另一个同理 这个数值大家自行调整
        ValueAnimator animator =  ValueAnimator.ofFloat(maxElasticFactor/4, (float) (maxElasticFactor / 1.5),0);
        animator.setDuration(duration/2);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                calc();//重新画路径
                nowElasticFactor= (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

再来一个重新绘画路径计算的方法

   public void calc(){

        //重置路径
        path.reset();
        //由于画文字是由基准线开始
        path.moveTo(iv.getX()-textWidth/2+iv.getWidth()/2, textHeight+iv.getHeight()+downHeight*iv.getHeight());
        //画二次贝塞尔曲线
        path.rQuadTo(textWidth / 2, nowElasticFactor, textWidth, 0);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

好了到这里我们看看完整源码吧:

package com.example.administrator.myapplication;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;


public class My extends FrameLayout {

    //画笔
    private Paint paint;
    //路径
    private Path path;
    //要输入的文本
    private String printText = "正在加载";
    //文本宽
    private float textWidth;
    //文本高
    private float textHeight;
    //测量文字宽高的时候使用的矩形
    private Rect rect;
    //最大弹力系数
    private float elastic = 1.5f;

    //最大弹力
    private float maxElasticFactor;

    //当前弹力
    private float nowElasticFactor;

    //用于记录当前图片使用数组中的哪张
    int indexImgFlag = 0;

    //下沉图片
    int allImgDown [] = {R.mipmap.p2,R.mipmap.p4,R.mipmap.p6,R.mipmap.p8};

    //动画效果一次下沉或上弹的时间 animationDuration*2=一次完整动画时间
    int animationDuration = 1000;

    //弹起来的图片
    ImageView iv;

    //图片下沉高度(即从最高点到最低点的距离)
    int downHeight = 2;
    private Animation translateDown;
    private Animation translateUp;

    private ObjectAnimator rotation;


    public My(Context context) {
        super(context);
    }

    public My(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.drawTextOnPath(printText, path, 0, 0, paint);
    }


    //用于播放文字下沉和上浮动画传入的数值必须是 图片下沉和上升的一次时间
    public void initAnimation(int duration){
        //这里为什maxElasticFactor/4为什么
        ValueAnimator animator =  ValueAnimator.ofFloat(maxElasticFactor/4, (float) (maxElasticFactor / 1.5),0);
        animator.setDuration(duration/2);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                calc();
                nowElasticFactor= (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }


    public void calc(){

        //重置路径
        path.reset();
        //由于画文字是由基准线开始
        path.moveTo(iv.getX()-textWidth/2+iv.getWidth()/2, textHeight+iv.getHeight()+downHeight*iv.getHeight());
        //画二次贝塞尔曲线
        path.rQuadTo(textWidth / 2, nowElasticFactor, textWidth, 0);

    }


    //初始化弹跳动画
    public void  MyAnmation(){
        //下沉效果动画
        translateDown = new TranslateAnimation(
                Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0,
                Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,downHeight);
        translateDown.setDuration(animationDuration);
        //设置一个插值器 动画将会播放越来越快 模拟重力
        translateDown.setInterpolator(new AccelerateInterpolator());


        //上弹动画
        translateUp = new TranslateAnimation(
         Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0,
          Animation.RELATIVE_TO_SELF,downHeight,Animation.RELATIVE_TO_SELF,0
       );

        translateUp.setDuration(animationDuration);
        ////设置一个插值器 动画将会播放越来越慢 模拟反重力
        translateUp.setInterpolator(new DecelerateInterpolator());


        //当下沉动画完成时播放启动上弹
        translateDown.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                iv.setImageResource(allImgDown[indexImgFlag]);
                rotation = ObjectAnimator.ofFloat(iv, "rotation", 180f, 360f);
                rotation.setDuration(1000);
                rotation.start();
            }


            @Override
            public void onAnimationEnd(Animation animation) {

                iv.startAnimation(translateUp);
                initAnimation(animationDuration);

            }


            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });

        //当上移动画完成时 播放下移动画
        translateUp.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                indexImgFlag = 1+indexImgFlag>=allImgDown.length?0:1+indexImgFlag;
                iv.setImageResource(allImgDown[indexImgFlag]);
                rotation = ObjectAnimator.ofFloat(iv, "rotation", 0.0f, 180f);
                rotation.setDuration(1000);

                rotation.start();
            }


            @Override
            public void onAnimationEnd(Animation animation) {
                //递归
                iv.startAnimation(translateDown);

            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });






    }

    boolean flagMeure;

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        init();
    }

    private void init() {

        //初始化弹跳图片 控件
        iv = new ImageView(getContext());

        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);

        iv.setLayoutParams(params);
        iv.setImageResource(allImgDown[0]);


        this.addView(iv);

        //画笔的初始化
        paint = new Paint();
        paint.setStrokeWidth(1);
        paint.setColor(Color.CYAN);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(50);
        paint.setAntiAlias(true);

        //矩形
        rect = new Rect();
        //将文字画入矩形目的是为了测量高度
        paint.getTextBounds(printText, 0, printText.length(), rect);

        //文本宽度
        textWidth = paint.measureText(printText);

        //获得文字高度
        textHeight = rect.bottom - rect.top;

        //初始化路径
        path = new Path();

        iv.setX(getWidth()/2);

        iv.measure(0,0);

        iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {

                if (!flagMeure)
                {
                    flagMeure =true;
                    //由于画文字是由基准线开始
                    path.moveTo(iv.getX()-textWidth/2+iv.getWidth()/2, textHeight+iv.getHeight()+downHeight*iv.getHeight());

                    //计算最大弹力
                    maxElasticFactor = (float) (textHeight / elastic);
                    //初始化贝塞尔曲线
                    path.rQuadTo(textWidth / 2, 0, textWidth, 0);

                    //初始化上弹和下沉动画
                    MyAnmation();

                    iv.startAnimation(translateDown);
                }
            }
        });




    }
}
  • 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
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268

小人奉上源码一封供大家 参考github源码下载地址

安卓高级 WebView的使用到 js交互

我们先来学习 怎么使用再到用js和安卓源生方法交互

WebView简单使用

此部分转载并做了补充 原博客
原因:比较简单不是很想在写,我只要写js交互部分

  1. WebView可以使得网页轻松的内嵌到app里,还可以直接跟js相互调用。

  2. webview有两个方法:setWebChromeClient 和 setWebClient

  3. setWebClient:主要处理解析,渲染网页等浏览器做的事情

  4. setWebChromeClient:辅助WebView处理Javascript的对话框,网站图标,网站title,加载进度等

  5. WebViewClient就是帮助WebView处理各种通知、请求事件的。

在AndroidManifest.xml设置访问网络权限:

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

控件:

<WebView 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/webView"
    />
  • 1
  • 2
  • 3
  • 4
  • 5

用途一:加载本地/Web资源
这里写图片描述
example.html 存放在assets文件夹内

调用WebView的loadUrl()方法,

加载本地资源

webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("file:///android_asset/example.html");
  • 1
  • 2

加载web资源:

webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("http://baidu.com");
  • 1
  • 2

用途二:在程序内打开网页

这里写图片描述

创建一个自己的WebViewClient,通过setWebViewClient关联

package com.example.testopen;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends Activity {
private WebView webView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test);             
        init();

    }


    private void init(){
        webView = (WebView) findViewById(R.id.webView);
        //WebView加载web资源
       webView.loadUrl("http://baidu.com");
        //覆盖WebView默认使用第三方或系统默认浏览器打开网页的行为,使网页用WebView打开
       webView.setWebViewClient(new WebViewClient(){
           @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            // TODO Auto-generated method stub
               //返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
             view.loadUrl(url);
            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

用途三:

如果访问的页面中有Javascript,则webview必须设置支持Javascript

//启用支持javascript
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
  • 1
  • 2
  • 3

用途四:

如果希望浏览的网页后退而不是退出浏览器,需要WebView覆盖URL加载,让它自动生成历史访问记录,那样就可以通过前进或后退访问已访问过的站点。

//改写物理按键——返回的逻辑
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // TODO Auto-generated method stub
        if(keyCode==KeyEvent.KEYCODE_BACK)
        {
            if(webView.canGoBack())
            {
                webView.goBack();//返回上一页面
                return true;
            }
            else
            {
                System.exit(0);//退出程序
            }
        }
        return super.onKeyDown(keyCode, event);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

用途五:判断页面加载过程

webView.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                // TODO Auto-generated method stub
                if (newProgress == 100) {
                    // 网页加载完成

                } else {
                    // 加载中

                }

            }
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

用途六:缓存的使用

优先使用缓存

webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
  • 1
webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
  • 1

本人补充

还有一些用法由于原作者没写所以我在这里补充下 从这里开始就是原创部分:

需求:假设后台给你返回的是html标签(没有头尾标签 简单说就是没有<html><heand></head><body></body></html>)

//假设返回的字符传如下所示:

package a.com.jswebproject.bean;

/**

 */
public class JString {
    public static final String  CONTENT = "<p style=\"text-indent:32px;line-height:200%;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\"><img src=\"http://s1.sns.maimaicha.com/images/2015/12/31/20151231082937_53817.jpg\" style=\"float:none;\" title=\"771c3d95184d9cb2f73a7d156d332df8.jpg\" border=\"0\" hspace=\"0\" vspace=\"0\" />光阴荏苒,</span><a href=\"http://www.sanwen.net/suibi/suiyue/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">岁月</span></a><span style=\"font-size:15px;\">飞逝如电。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">回眸花落时,</span><span style=\"font-size:15px;font-family:';\">201</span><span style=\"font-size:15px;font-family:';\">5</span><span style=\"font-size:15px;\">就这样悄然而过</span><span style=\"font-size:15px;font-family:';\">……</span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">清浅时光,积聚如山的往事随风游走,</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">带着我们内心所有的牵念,尘封于深深的</span><a href=\"http://huiyi.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">记忆</span></a><span style=\"font-size:15px;\">里。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">记忆,从此被定格!</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">心生温暖,四季平安!每逢岁杪,将昔年一一盘点</span><span style=\"font-size:15px;font-family:';\">……</span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">那一路,我们</span><a href=\"http://cengjing.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">曾经</span></a><span style=\"font-size:15px;\">怎样走过?</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">那一程,谁又曾从心坎上路过?</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">诚然,认识了一些人,却也经历过许多的事。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">浮生若</span><span style=\"font-size:15px;font-family:';\"><a href=\"http://meng.sanwen8.cn/\" target=\"_blank\"><span style=\"font-family:宋体;color:#444444;\">梦</span></a></span><span style=\"font-size:15px;\">,尘缘辗转。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">在心里,就让那些愁殇,随风飘逝吧!</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">站在</span><span style=\"font-size:15px;font-family:';\">201</span><span style=\"font-size:15px;font-family:';\">5</span><span style=\"font-size:15px;\">与</span><span style=\"font-size:15px;font-family:';\">201</span><span style=\"font-size:15px;font-family:';\">6</span><span style=\"font-size:15px;\">年的界碑上,不禁忍不住再次回首</span><span style=\"font-size:15px;font-family:';\">——</span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">到底,有多少得失能够沉淀于心?</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">究竟,有多少</span><a href=\"http://huiyi.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">回忆</span></a><span style=\"font-size:15px;\">值得</span><a href=\"http://yongheng.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">永久</span></a><span style=\"font-size:15px;\">珍藏?</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<a href=\"http://shengming.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">生命</span></a><span style=\"font-size:15px;\">中,总有一些人会成为彼此的匆匆过客;</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">岁月里,总有一些事会流逝而淡出我们的心际;</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">经年间,总有一些</span><a href=\"http://www.sanwen.net/sanwen/xinqing/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">情感</span></a><span style=\"font-size:15px;\">在磨砺中教会我们成熟。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">经历</span><a href=\"http://rensheng.sanwen.net/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">人生</span></a><span style=\"font-size:15px;\">中的点点滴滴,阅历因此而丰硕起来。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">学会淡定从容,坦然</span><a href=\"http://www.sanwen.net/suibi/shenghuo/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">生活</span></a><span style=\"font-size:15px;\">;</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">学会心存善念,静泊尘心。</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">活在当下,最美!在当下,</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">给与</span><a href=\"http://xiangxinziji.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">自己</span></a><span style=\"font-size:15px;\">一份简单的期许,又或是</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">一个</span><a href=\"http://danchun.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">纯真</span></a><span style=\"font-size:15px;\">的祈愿!</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">让明天安然,让未来更好!</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">跨年之</span><a href=\"http://ye.sanwen8.cn/\" target=\"_blank\"><span style=\"font-size:15px;color:#444444;\">夜</span></a><span style=\"font-size:15px;\">,我倚窗凝望,北极星光,绚烂如花!</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<p style=\"text-indent:32px;line-height:200%;text-align:left;margin-bottom:24px;\">\n" +
            "\t<span style=\"font-size:15px;\">祈愿,</span><span style=\"font-size:15px;font-family:';\">201</span><span style=\"font-size:15px;font-family:';\">6</span><span style=\"font-size:15px;\">年每一个阳光灿烂的日子,</span><span style=\"font-size:15px;font-family:';\"></span> \n" +
            "</p>\n" +
            "<span style=\"font-size:15px;\">佑你,佑我,佑他!</span> \n" +
            "<p>\n" +
            "\t<br />\n" +
            "</p>";

}
  • 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

那我们看看 具体代码怎么加载上面的文字吧

package a.com.jswebproject;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.webkit.WebChromeClient;
import android.webkit.WebView;

import qianfeng.com.jswebproject.bean.JString;

public class JsonActivity extends AppCompatActivity {
    private WebView mWebView;

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

    private void setData() {
        //如果你直接new WebChromeClient() 或者 new WebClient()默认在程序内打开
        mWebView.setWebChromeClient(new WebChromeClient());
        //params1  baseUrl 基地址 如果你需要在加载的页面里面进行相应操作,那么提交的网址会在基地址的基础上进行添加
        //params2  你要加载的html
        //params3 你加载的html的类型 (text/html) (text/javascript)
        //params4 编码  "UTF-8" "GBK"
        //params5 你访问历史路径
        //http://baidu.com?username=lla&password=123456;
        mWebView.loadDataWithBaseURL(null, JString.CONTENT,"text/html","UTF-8",null);
    }

    private void initView() {
        mWebView = (WebView) findViewById(R.id.wv_json_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
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

再来看个有加载动画的案例 并具有刷新后退前进功能的
这里写图片描述

package qianfeng.com.jswebproject;

import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.ProgressBar;

import qianfeng.com.jswebproject.client.MyChormeClient;
import qianfeng.com.jswebproject.client.MyWebViewClient;

public class NetActivity extends AppCompatActivity {
    private WebView mWebView;
    private ActionBar mActionBar;
    private ProgressBar mProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_net);
        initView();
        initData();
        setData();
        setListener();
    }

    private void initView() {
      mWebView = (WebView) findViewById(R.id.wv_net_test);
      mActionBar =  getSupportActionBar();
      mProgressBar = (ProgressBar) findViewById(R.id.pb_net_show);
    }

   public void onClick(View view){
       if (view!=null){
           switch (view.getId()){
               case R.id.bt_net_advance:
                   if (mWebView.canGoForward()){
                       mWebView.goForward();
                   }
                   break;
               case R.id.bt_net_back:
                   if (mWebView.canGoBack()){
                       mWebView.goBack();
                   }
                   break;
               case R.id.bt_net_refresh:
                   // WebView从新加载(刷新)
                   mWebView.reload();
                   break;
               case R.id.bt_net_stop:
                   // WebView停止加载
                   mWebView.stopLoading();
                   break;
               default:
                   break;
           }
       }
   }

    private void setListener() {
    }

    private void setData() {
        // 加载网址,要确定你的网址是正确的,然后网络正常,最后权限(联网权限)
        mWebView.loadUrl("http://baidu.com");
        // WebView在加载网页时候需要设置一个WebViewClient 用来监听网络加载开始和介绍的
        // 如果你不设置为客户端,他就会调用系统默认的浏览器来给你加载网页
        //mWebView.setWebViewClient(new WebViewClient());
        MyWebViewClient webViewClient = new MyWebViewClient();
        webViewClient.setClientListener(new MyWebViewClient.ClientCallBack() {
            @Override
            public void onStart(String url) {
                // 设置控件显示和隐藏或者消失
                mProgressBar.setVisibility(View.VISIBLE);
            }

            @Override
            public void onFinish(String url) {
             mProgressBar.setVisibility(View.GONE);
            }
        });
        mWebView.setWebViewClient(webViewClient);
        // 给WebView设置一个ChormeClient,来检测网页加载进度和收到的标题
       // mWebView.setWebChromeClient(new WebChromeClient());
        mWebView.setWebChromeClient(new MyChormeClient());
        MyChormeClient client = new MyChormeClient();
        client.setChormeListener(new MyChormeClient.ChormeCallBack() {
            @Override
            public void onProgressChanged(int progress) {
                // 给ProgressBar设置进度
                mProgressBar.setProgress(progress);

            }

            @Override
            public void onReceivedTitle(String title) {
              if (!TextUtils.isEmpty(title)){
                  mActionBar.setTitle(title);
              }
            }
        });
        mWebView.setWebChromeClient(client);
        // 获取WebView的基本设置
        WebSettings settings = mWebView.getSettings();
        // 设置和js交互是否可用
        settings.setJavaScriptEnabled(true);
        mWebView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

    }

    private void initData() {
    }
    // 当用户按下返回键的时候,系统就会调用这个方法
    @Override
    public void onBackPressed() {
        // 判断WebView是否能够返回
        if (mWebView.canGoBack()){
            // 如果能返回,就返回WebView的内容
            mWebView.goBack();
        }else {
            super.onBackPressed();
        }

    }
}
  • 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

JS方法调用安卓方法

  1. 我们创建一个类 用于给js交互
    如果你的方法想给js调用,此方法必须加上注解@JavascriptInterface

     class JS {
    
            //如果此方法想 被js调用必须写此注解
            @JavascriptInterface
            public void showToast(String msg){
                Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
            }
    
            @JavascriptInterface
            public void sub(int a,int b){
                Toast.makeText(MainActivity.this, (a+b)+"", Toast.LENGTH_SHORT).show();
            }
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  2. 添加此类到webView中

//第二个参数随意 当HTML5工程师想调用js方法时
// 第二个参数名字.方法名 
//如:Android.sub(20,30)
   JS js = new JS();
   webView.addJavascriptInterface(js,"Android");
  • 1
  • 2
  • 3
  • 4
  • 5
  1. js 使用时 调用
    先来看看html 源码吧
<html>
<meta charset="UTF-8">
<head>
    <title>这是我的第一个html</title>
    <script type="text/javascript">
        function add (a ,b){
         var count = a+b;
         var textHtml = document.getElementById("result_text");
         textHtml.innerHTML = count;
        }
        function showToast(msg){
          Android.showToast(msg);
        }

        function sub(a,b){
          Android.sub(a,b);
        }
        function test(msg){
         var textHtml = document.getElementById("result_text");
         textHtml.innerHTML = msg;
        }


    </script>
</head>
<body>
<h1>这是啥啊</h1>
<h2>这是啥啊</h2>
<h3>这是啥啊</h3>
<h4>这是啥</h4>

<p>CSDN的朋友们一起学习</p>
<input value="这是一个button" type="button" onclick="javascript:alert('大家好')"><br/>
<input value="点击我试试" type="button" onclick="add(20,10)"><br/>
<input value="点击调用Android显示一个Toast" type="button" onclick="showToast('这是来着网页的文本')"><br/>
<input value="点击调用Android 进行一个减法" type="button" onclick="sub(90,10)">

<a href="https://baidu.com">点击去百度</a>

<form>
    <label><input type="text" name="username"></label>
    <label><input type="text" name="password"></label>
    <input type="submit" name="点击提交">

</form>

<div id="result_text"></div>

</body>

</html>
  • 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

核心部分:

 <script type="text/javascript">

        function showToast(msg){
          Android.showToast(msg);
        }

        function sub(a,b){
          Android.sub(a,b);
        }
    </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

//感谢同学们
这里写图片描述

安卓调用JS

  webView.loadUrl("javascript:test('你好啊朋友')");
  • 1

test为js中的方法

function test(msg){
         var textHtml = document.getElementById("result_text");
         textHtml.innerHTML = msg;
        }
  • 1
  • 2
  • 3
  • 4

好了大家看下完整一点的代码吧

package com.example.myapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private WebView webView;
    private WebSettings settings;

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

    private void initView() {
        webView = (WebView) findViewById(R.id.webView);
        webView.setWebChromeClient(new WebChromeClient());
        settings = webView.getSettings();
        settings.setJavaScriptEnabled(true);
        webView.loadUrl("file:///android_asset/haha.html");
        JS js = new JS();
        webView.addJavascriptInterface(js,"Android");

    }


    class JS {

        //如果此方法想 被js调用必须写此注解
        @JavascriptInterface
        public void showToast(String msg){
            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
        }

        @JavascriptInterface
        public void sub(int a,int b){
            Toast.makeText(MainActivity.this, (a+b)+"", Toast.LENGTH_SHORT).show();
        }
    }

    public void onClick(View view) {
        webView.loadUrl("javascript: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
  • 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

源码:github源码

安卓热修复之AndFIX

我致力于最新的前沿安卓技术分析和使用教学,不打算将很多很深的东西,因为有多少人愿意沉下你的心境去学习难点?我一般只会简单提及.文字错漏在所难免还希望同学们喜欢

热修复介绍

热修复是什么? 如果你一个项目已经上线,出现了严重缺陷,那么你第一反应是推送新版本.那么问题来.老子刚下你的APP 你就叫我重新下载?啥东西!卸了.从而导致用户流量的减退.而热修复就是推送一个补丁文件到客户端(很小),用户打开应用时自动安装.是不是很棒?

AndFix 热修复原理

已经翻译的源码分析

阿里github官网(英文)

推荐自行看官网
如果时间上允许我会帮大家翻译完整版的

AndFix注意的地方

  1. 不支持添加字段 方法 资源 布局修改 类
  2. 虽然不支持添加字段 但是你可以在方法内添加 局部变量(后面解释)

错误:
1. 如果添加了在生成补丁包会有如下错误(大家先看看不需要先知道怎么生成,后面阐述)
这里写图片描述

成功:
仔细看 上面的文字会有什么add modifie(添加修改) com.xxx(你修改的文件)
这里写图片描述

AndFix使用

  1. 首先添加依赖

    dependencies {
            compile 'com.alipay.euler:andfix:0.5.0@aar'
    }
    
    • 1
    • 2
    • 3
    • 4
  2. 初始化 一般在application的实现类
    我们首先创建一个类继承之 application 然后在oncreat方法初始化,
    我先介绍几个API:

    此API返回你的应用版本名()
    java
    String appversion; appversion=getPackageManager().getPackageInfo(getPackageName(),0).ersionName;

    这里写图片描述

    再来看个依赖包的类PatchManager

     public PatchManager patchManager ;
    • 1

    此类有如下几个方法

    参数为上下文 一般都在application中

    patchManager = new PatchManager(this);
    • 1
        初始化 这里传入的是版本名 可以用上面介绍的一个api填入 也可以直接写死
    
    • 1
    • 2
     patchManager.init("1.0");
     patchManager.init(versionName);
    • 1
    • 2

    初始化加载补丁 (你直接当写死就行一般默认在new出来并初始版本后调用.这里还是不真正的加载方法)

      patchManager.loadPatch();
    • 1

    打入补丁 参数为字符串 打入后生效 路径你随意写 文件名也是一样
    addPatch(你的补丁路径)
    //移除补丁路径
    patchManager.removeAllPatch();

    String path = Environment.getExternalStorageDirectory().getAbsoluteFile() + File.separator + "out.apatch";
    
            File file = new File(path);
            if (file.exists()){
                Log.e("fmy","文件存在");
                try {
                    patchManager.addPatch(path);
                    patchManager.removeAllPatch();
                    Log.e("fmy","热修复成功");
                } catch (IOException e) {
                    Log.e("fmy","热修复失败:"+e.getMessage());
                }
            }else{
                Log.e("fmy","文件不存在");
            }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    完整代码:

    package com.example.administrator.myapplication;
    
    import android.app.Application;
    import android.os.Environment;
    import android.util.Log;
    
    import com.alipay.euler.andfix.patch.PatchManager;
    
    import java.io.File;
    import java.io.IOException;
    
    /**
     * Created by Administrator on 2016/11/9.
     */
    
    public class MainAppliacation extends Application {
    
    
        public PatchManager patchManager ;
        private String path;
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            patchManager = new PatchManager(this);
            patchManager.init("1.0");
            patchManager.loadPatch();
    
            path = Environment.getExternalStorageDirectory().getAbsoluteFile() + File.separator + "out.apatch";
    
            File file = new File(path);
            if (file.exists()){
                Log.e("fmy","文件存在");
                try {
                    patchManager.addPatch(path);
                    patchManager.removeAllPatch();
                    Log.e("fmy","热修复成功");
                } catch (IOException e) {
                    Log.e("fmy","热修复失败:"+e.getMessage());
                }
            }else{
                Log.e("fmy","文件不存在");
            }
    
    
        }
    }
    
    • 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
  3. 添加权限和把我们自定义application关联上
    权限:

     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    • 1
    • 2

    //关联我们自定义application类

     <application
            android:name=".MainAppliacation"
            ....
    • 1
    • 2
    • 3

    完整清单文件:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.administrator.myapplication">
    
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    
        <application
            android:name=".MainAppliacation"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  4. 生成新旧版本的两个已经签名的APK(签名需一样)
    假设我们原本的apk 两个类 一个是activity 类 和一个我们自定义的application类(上面已经说了)

    我们看看activity 文件

    package com.example.administrator.myapplication;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity {
    
        String name ="你好";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        //一个按钮的点击事件
        public void onClick(View view) {
    
            Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    看下布局文件吧

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.example.administrator.myapplication.MainActivity">
    
        <Button
            android:onClick="onClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="看看热修复是否成功" />
    </RelativeLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    大概的效果图
    这里写图片描述
    打包签名此apk 并取名为old.apk(这里是为了后面好讲此命名)

    修改一下activity文件
    修改了name的数值

    package com.example.administrator.myapplication;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity {
    
        String name ="修复了!!!!";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        //一个按钮的点击事件
        public void onClick(View view) {
    
            Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
        }
    
    
    }
    
    • 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

    打包签名此apk 并取名为new.apk(这里是为了后面好讲此命名)

  5. 下载生成补丁工具
    阿里github官网(英文)

    在github下载源码不用我教吧?把阿里整个master下载到本地 并解压
    ![这里写图片描述](http://img.blog.csdn.net/20161109113056564)
    
    • 1
    • 2
    • 3

    打开上述目录的tools目录 解压工具包到你喜欢的位置

    这里写图片描述

    现在我打开我解压的后的目录
    这里写图片描述

  6. 将我们生成的new.apk 和old.apk 还有签名文件一并放入此文件夹
    这里写图片描述

  7. 生成补丁
    在此目录下按下Shit键+鼠标右键 选择 可以看到一个选项为”在此处打开一个命令窗口” —>>不好截图 一点截图就关掉了所以 I’m so sorry

    在跳出命令窗口输入如下命令

    命令 : apkpatch.bat -f new.apk -t old.apk -o output1 -k debug.keystore -p android -a androiddebugkey -e android
    
    -f <new.apk> :新版本
    -t <old.apk> : 旧版本
    -o <output> : 输出目录
    -k <keystore>: 打包所用的keystore
    -p <password>: keystore的密码
    -a <alias>: keystore 用户别名
    -e <alias password>: keystore 用户别名密码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    提示在 dos 窗口按下tab 输入此文件夹下的某个文件名首字母或者一段可以提示哦
    这里写图片描述

-o o 我这里的用法是 在此目录下创建一个o文件夹然后将生产的东西放入此文件夹
  • 1

我们打开看看我们的o文件夹有什么
这里写图片描述

可以看到有一个叫
“new-4d459be7271c372742862b1a885b176a.apatch”的文件
这个就是我们需要的 其他文件用处我以后会补充的 如加固的一些问题

我们把此文件改名 “out.apatch”

现在呢我们演示一下吧:
首先安装old.apk 到手机中 我们运行看看
这里写图片描述

我们把文件out.apath导入到此模拟器的sd卡根目录 在关闭程序(记得清后台!!!!!) 不然你不会重新调用application的oncreat

这里写图片描述

What ?为什么没有变成我们后面改成的字符串?其实是我故意写下的一个坑 我的目的很简单 如果让同学一帆风顺的写下来你永远不会影响深刻对AndFIX哪些部分可以热修复 如前文 布局文件等

这里之所以 不出来 是因为他把重新再name的类中赋值当成重新生成了一个字段(前面说过新字段不可以热修复),所以解锁方法如下

package com.example.administrator.myapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    String name ="你好";

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

    //一个按钮的点击事件
    public void onClick(View view) {
        name ="修复了";

        Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
    }


}
  • 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

然后重新生成一个new.apk 在次和签名文件生成一个新的补丁效果就出来了.这里我就不重复无聊的步骤了,我现在导入正确的补丁
这里写图片描述
(^__^) 我很无聊吧?

混淆

# 先版本两个字换成你的发布版本 一般是release
#debug
# release
-applymapping build/outputs/mapping/换成你的版本(一般是release版本)/mapping.txt
-keep class * extends java.lang.annotation.Annotation
-keep class com.alipay.euler.** {*;}
-keepclasseswithmembernames class * { # 保持 native 方法不被混淆
   native <methods>;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

第一次编译的时候注释这行(前面加个#号)

-applymapping build/outputs/mapping/换成你的版本(一般是release版本)/mapping.txt
  • 1