超星雅儿跳课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:得到思路 作者应该每次拖动的时候判断 你以上看到哪了,或者是否看完了。

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

思路和源码(已加密)

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

android注解入门 并来自己写一个框架

介绍

这里我带大家来学习一下注解 并且用来写下一个模仿xUtils3 中View框架
此框架 可以省略activity或者fragment的 findViewById 或者设置点击事件的烦恼
我正参加2016CSDN博客之星的比赛 希望您能投下宝贵的一票,点击进入投票
我的github上的源码,包含doc和使用说明

如下代码:

fragment

package a.fmy.com.myapplication;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import a.fmy.com.mylibrary.FmyClickView;
import a.fmy.com.mylibrary.FmyContentView;
import a.fmy.com.mylibrary.FmyViewInject;
import a.fmy.com.mylibrary.FmyViewView;

//你的fragment的布局id  Your fragment's LayoutId
@FmyContentView(R.layout.fragment_blank)
public class BlankFragment extends Fragment {
    //你想实例化控件的id
    //Do you want to control instance id
    // 等价于 findViewByid
    //Equivalent to the findViewByid
    @FmyViewView(R.id.tv1)
    TextView tv1;
    @FmyViewView(R.id.tv2)
    TextView tv2;
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
       //初始化fragment Initialize Fragement
        return FmyViewInject.injectfragment(this,inflater,container);
    }
    //你想给哪个控件添加 添加事件 的id
    //Do you want to add add event id to which controls
    @FmyClickView({R.id.tv1,R.id.tv2})
    public void myOnclick(View view){
        switch (view.getId()) {
            case R.id.tv1:
                tv1.setText("TV1  "+Math.random()*100);
                break;
            case R.id.tv2:
                tv2.setText("TV2  "+Math.random()*100);
                break;
            default:

        }

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

Activity

package a.fmy.com.myapplication;

import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.widget.FrameLayout;
import a.fmy.com.mylibrary.FmyContentView;
import a.fmy.com.mylibrary.FmyViewInject;
import a.fmy.com.mylibrary.FmyViewView;

@FmyContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @FmyViewView(R.id.fl)
    FrameLayout fl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //initActivity
        // 初始化activity
        FmyViewInject.inject(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.add(R.id.fl,new BlankFragment());
        fragmentTransaction.commit();
    }
}
  • 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

java注解学习

java注解教学大家点击进入大致的看一下即可 不然我不知道这篇博客需要写多久

activity设置填充布局框架

这里我们先写一个用于activity框架 你学习完了之后其实你也会fragment了.
1. 实现activity不需要调用setContentView(R.layout.activity_main);此方法完成布局填充 我们看下效果
不使用框架:

package a.fmy.com.mylibrary;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

使用框架:

package a.fmy.com.mylibrary;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
@FmyContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
            FmyViewInject.inject(this);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

第一步:
创建一个注解类如下
@Target —>>此注解在什么地方可以使用 如类还是变量
ElementType.TYPE只能在类中使用此注解
@Retention(RetentionPolicy.RUNTIME) 注解可以在运行时通过反射获取一些信息(这里如果你疑惑那么请六个悬念继续向下看)

/**
 * 此方注解写于activity类上 可以免去 setContentView()步骤 
 * @author 范明毅
 * @version 1.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FmyContentView {  
    /**
     * 保存布局文件的id eg:R.layout.main
     * @return 返回 布局id
     */
    int value();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

第二步:
写一个工具类 配合注解使用 当开发者使用此类时激活注解的作用

public class FmyViewInject {
    /**
     * 保存传入的activity
     */
    private static Class<?> activityClass;
    /**
     * 初始化activity和所有注解
     * 
     * @param obj
     *            你需要初始化的activity
     */
    public static void inject(Object obj) {
    }

    /**
     * 初始化activity布局文件 让其不用调用setContentView
     * 
     * @param activity
     */
    private static void injectContent(Object obj) {
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

大家先不用着急看不懂为什么这样写原因

核心源码位于injectContent 我们来实现此方法

    /**
     * 初始化activity布局文件 让其不用调用setContentView
     * 
     * @param activity
     */
    private static void injectContent(Object obj) {

        // 获取注解
        FmyContentView annotation = activityClass
                .getAnnotation(FmyContentView.class);

        if (annotation != null) {
            // 获取注解中的对应的布局id 因为注解只有个方法 所以@XXX(YYY)时会自动赋值给注解类唯一的方法
            int id = annotation.value();
            try {
                // 得到activity中的方法 第一个参数为方法名 第二个为可变参数 类型为 参数类型的字节码
                Method method = activityClass.getMethod("setContentView",
                        int.class);

                // 调用方法 第一个参数为哪个实例去掉用 第二个参数为 参数
                method.invoke(obj, id);
            } 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

此方法写完后工具类的inject()方法调用即可

    /**
     * 初始化activity和所有注解
     * 
     * @param obj
     *            你需要初始化的activity
     */
    public static void inject(Object obj) {
        activityClass = obj.getClass();
        // 初始化activity布局文件
        injectContent(obj);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

完整代码:

public class FmyViewInject {
    /**
     * 保存传入的activity
     */
    private static Class<?> activityClass;
    /**
     * 初始化activity和所有注解
     * 
     * @param obj
     *            你需要初始化的activity
     */
    public static void inject(Object obj) {
        activityClass = obj.getClass();
        // 初始化activity布局文件
        injectContent(obj);
    }
    /**
     * 初始化activity布局文件 让其不用调用setContentView
     * 
     * @param activity
     */
    private static void injectContent(Object obj) {

        // 获取注解
        FmyContentView annotation = activityClass
                .getAnnotation(FmyContentView.class);

        if (annotation != null) {
            // 获取注解中的对应的布局id 因为注解只有个方法 所以@XXX(YYY)时会自动赋值给注解类唯一的方法
            int id = annotation.value();
            try {
                // 得到activity中的方法 第一个参数为方法名 第二个为可变参数 类型为 参数类型的字节码
                Method method = activityClass.getMethod("setContentView",
                        int.class);

                // 调用方法 第一个参数为哪个实例去掉用 第二个参数为 参数
                method.invoke(obj, id);
            } 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

赶快去试试 我们继续写下一步 用法在开始的示例有

activity查找控件

效果如下

@FmyContentView(R.layout.activity_main)
public class MainActivity extends FragmentActivity {
    //直接实例化
    @FmyViewView(R.id.fl)
    private FrameLayout fl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FmyViewInject.inject(this);


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

第一步:
继续写一个注解

/**
 * 此方注解写于activity类中 控件变量上 可以省去findViewId 的烦恼
 * @author 范明毅
 * @version 1.0
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FmyViewView {
    /**
     * 保存view控件的id
     * @return view控件id
     */
    int value();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

第二步 继续第一节的”activity设置填充布局框架”中的工具类添加新的方法

 /**
     * 初始化activity中的所有view控件 让其不用一个findViewid 实例化
     *
     * @param activity
     */
    private static void injectView(Object activityOrFragment) {

        // 对象所有的属性
        Field[] declaredFields = null;


        // 健壮性
        if (activityClass != null) {
            // 获取du所有的属性 包含私有 保护 默认 共开 但不包含继承等
            // getFields可以获取到所有公开的包括继承的 但无法获取到私有的属性
            declaredFields = activityClass.getDeclaredFields();
        }


        // 健壮性
        if (declaredFields != null) {
            // 遍历所有的属性变量
            for (Field field : declaredFields) {

                // 获取属性变量上的注解
                FmyViewView annotation = field.getAnnotation(FmyViewView.class);

                // 如果此属性变量 包含FMYViewView
                if (annotation != null) {
                    // 获取属性id值
                    int id = annotation.value();

                    Object obj = null;
                    try {

                        // 获取activity中方法
                        obj = activityClass.getMethod("findViewById",
                                int.class).invoke(activityOrFragment, id);


                        Log.e("FMY", "" + field.getClass());
                        // 设置属性变量 指向实例

                        // 如果修饰符不为公共类 这里注意了 当activity
                        // 控件变量为private的时候 我们去访问会失败的 要么打破封装系 要么变量改为public
                        //如 private TextView tv 这种情况 如果不打破封装会直接异常
                        if (Modifier.PUBLIC != field.getModifiers()) {
                            // 打破封装性
                            field.setAccessible(true);
                        }
                        // 这里相当于 field= acitivity.obj
                        field.set(activityOrFragment, obj);
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        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
  • 58
  • 59
  • 60
  • 61
  • 62

第三步
在工具类中的inject ()方法调用

    /**
     * 初始化activity和所有注解
     *
     * @param obj 你需要初始化的activity
     */
    public static void inject(Object obj) {

        activityClass = obj.getClass();

        // 初始化activity布局文件
        injectContent(obj);

        // 初始化所有控件实例 省去findViewId的痛苦
        injectView(obj);

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

activity设置控件的点击事件

这里需要的知识点 如动态代理等 这里大家可以自己百度看下
效果如下

@FmyContentView(R.layout.activity_main)
public class MainActivity extends FragmentActivity {

    @FmyViewView(R.id.fl)
    private FrameLayout fl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FmyViewInject.inject(this);


    }

    //当填充的布局中 id为R.id.fl 被点击将调用如下方法
    @FmyClickView({R.id.fl})
    public void onClick(View v){
        Log.e("fmy", "===>>");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

第一步 :
同样写下一个注解

/**
 * 
 * 设置点击事件的注解 只需要在某方法 上写上此注解即可 如@FmyClickView({R.id.bt1,R.id.bt2})
 * @version 1.0
 * @author 范明毅
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FmyClickView {
    /**
     * 保存所有需要设置点击事件控件的id
     * @return 
     */
    int [] value();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

第二步:
写下一个代理处理类(我写在工具类中)

/**
     * 代理处理点击逻辑代码
     * 
     * @author 范明毅
     *
     */
    static class MInvocationHandler implements InvocationHandler {
        //这里我们到时候回传入activity
        private Object target;

        // 用户自定义view 的点击事件方法
        private Method method;

        public MInvocationHandler(Object target, java.lang.reflect.Method method) {
            super();
            this.target = target;
            this.method = method;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            // 调用用户自定义方法的点击事件 让activity调用中开发者设定的方法 
            return this.method.invoke(target, args);
        }

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

第三步:
在工具类中写下一个方法用于初始化点击事件

    /**
     * 初始化所有控件的点击事件 只需要某方法上写上对应注解和id即可
     * 
     * @param activity
     */
    private static void inijectOnClick(Object activityOrFragment) {

        //获得所有方法
        Method[] methods  = null;


             methods = activityClass.getMethods();



        // 遍历所有的activity下的方法
        for (Method method : methods) {
            // 获取方法的注解
            FmyClickView fmyClickView = method
                    .getAnnotation(FmyClickView.class);
            // 如果存在此注解
            if (fmyClickView != null) {

                // 所有注解的控件的id
                int[] ids = fmyClickView.value();

                // 代理处理类
                MInvocationHandler handler = new MInvocationHandler(activityOrFragment,
                        method);

                // 代理实例 这里也可以返回     new Class<?>[] { View.OnClickListener.class }中的接口类
                //第一个参数用于加载其他类 不一定要使用View.OnClickListener.class.getClassLoader() 你可以使用其他的
                //第二个参数你所实现的接口
                Object newProxyInstance = Proxy.newProxyInstance(
                        View.OnClickListener.class.getClassLoader(),
                        new Class<?>[] { View.OnClickListener.class }, handler);

                // 遍历所有的控件id 然后设置代理
                for (int i : ids) {
                    try {
                        Object view = null;

                    //如果对象是activity

                             view = activityClass.getMethod("findViewById",
                                        int.class).invoke(activityOrFragment, i);


                        if (view != null) {
                            Method method2 = view.getClass().getMethod(
                                    "setOnClickListener",
                                    View.OnClickListener.class);
                            method2.invoke(view, newProxyInstance);
                        }
                    } 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
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

第四部:
在工具类的inject()调用即可


    /**
     * 初始化activity和所有注解
     * 
     * @param obj
     *            你需要初始化的activity
     */
    public static void inject(Object obj) {

        activityClass = obj.getClass();

        // 初始化activity布局文件
        injectContent(obj);

        // 初始化所有控件实例 省去findViewId的痛苦
        injectView(obj);

        // 初始化所有控件的点击事件
        inijectOnClick(obj);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

安卓onTextChanged参数解释及实现EditText字数监听 Editable使用

尊重原作者:此篇文章是借鉴原作者地址 的博文 并进行修改和增加补充说明,我只是补充和修改:
我感觉这篇文章经过我的补充 市面多少文本操作变化 你都知道怎么做了.并且感觉是非常详细关于 android 文本编辑框的文本变化 并且通俗易懂(内含动态图),

为了大家方便查看 我这里复制作者博文内容 并且修正部分内容 后面在补充
我正参加CSDN明日之星比赛 还希望您投我一票

原作者部分(修改部分)

由于最近做项目要检测EditText中输入的字数长度,从而接触到了Android中EditText的监听接口,TextWatcher。
它有三个成员方法,第一个after很简单,这个方法就是在EditText内容已经改变之后调用,重点看下面两个方法:

beforeTextChanged(CharSequence s, int start, int count, int after)
  • 1

这个方法是在Text改变之前被调用,它的意思就是说在原有的文本s中,从start开始的count个字符将会被一个新的长度为after的文本替换,注意这里是将被替换,还没有被替换。

onTextChanged(CharSequence s, int start, int before, int count)
  • 1

这个方法是在Text改变过程中触发调用的,它的意思就是说在原有的文本s中,从start开始的count个字符替换长度为before的旧文本,注意这里没有将要之类的字眼,也就是说一句执行了替换动作。
可能说起来比较抽象,我举个简单的例子,比如说我们监听一个EditText,默认开始的时候EditText中没有文本,当我们输入LOVE四个字母的时候,在打印信息中我输出各个参数看一下参数的变化。

10-18 16:40:21.528: D/Debug(4501): beforeTextChanged 被执行----> s=----start=0----after=1----count=0
10-18 16:40:21.528: D/Debug(4501): onTextChanged 被执行---->s=L----start=0----before=0----count=1
10-18 16:40:21.532: D/Debug(4501): afterTextChanged 被执行---->L
10-18 16:40:29.304: D/Debug(4501): beforeTextChanged 被执行----> s=L----start=1----after=1----count=0
10-18 16:40:29.308: D/Debug(4501): onTextChanged 被执行---->s=LO----start=1----before=0----count=1
10-18 16:40:29.308: D/Debug(4501): afterTextChanged 被执行---->LO
10-18 16:40:32.772: D/Debug(4501): beforeTextChanged 被执行----> s=LO----start=2----after=1----count=0
10-18 16:40:32.772: D/Debug(4501): onTextChanged 被执行---->s=LOV----start=2----before=0----count=1
10-18 16:40:32.776: D/Debug(4501): afterTextChanged 被执行---->LOV
10-18 16:40:34.772: D/Debug(4501): beforeTextChanged 被执行----> s=LOV----start=3----after=1----count=0
10-18 16:40:34.772: D/Debug(4501): onTextChanged 被执行---->s=LOVE----start=3----before=0----count=1
10-18 16:40:34.776: D/Debug(4501): afterTextChanged 被执行---->LOVE
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

通过上面的打印信息我们可以发现在输入L之前beforeTextChanged被执行,s为空,所以s输入空,start=0,也就是从位置0开始,count=0,也就是0个字符将会被替换,after=1,也就是说0个字符将会被一个新的长度为after=1的文本(也就是L)替换。
当输入发生改变的时候onTextChanged被执行,此时s=L也就是输入的字母L,从start=0开始,count=1个字符替换了长度为before=0的旧文本。通俗点将就是字母L从位置0开始替换了原来的空文本,下面的就可以依次类推了。那么我们如何利用这个接口监听EditText的文本变化来实现限制输入字数的功能呢,我相信大家都有自己的想法了,这里我给出自己的一个简单实现,主要代码如下:

source_des.addTextChangedListener(new TextWatcher() { 
    private CharSequence temp; 
    private int selectionStart; 
    private int selectionEnd; 

    @Override 
    public void onTextChanged(CharSequence s, int start, int before, int count) { 
        Log.d(TAG, "onTextChanged 被执行---->s=" + s + "----start="+ start 
          + "----before="+before + "----count" +count); temp = s; 
    }

    public void beforeTextChanged(CharSequence s, int start, int count,int after) { 
        Log.d(TAG, "beforeTextChanged 被执行----> s=" + s+"----start="+ start 
          + "----after="+after + "----count" +count); 
    } 

    public void afterTextChanged(Editable s) { 
        Log.d(TAG, "afterTextChanged 被执行---->" + s); 
        //获取光标开始的位置
        selectionStart = source_des.getSelectionStart(); 
        //获取光标结束的位置
        selectionEnd = source_des.getSelectionEnd(); 
        //这里其实selectionStart  == selectionEnd 
        // 大家可以把获取的位置放入beforeTextChanged 然后选择部分文字(选择部分位置用光标选择2个以上) 删除可以看到效果 我后面做实验
        if (temp.length() > MAX_LENGTH) { 
            Toast.makeText(MainActivity.this, "只能输入九个字", 
              Toast.LENGTH_SHORT).show();
              //删除部分字符串 为[x,y) 包含x位置不包含y
              //也就是说删除 位置x到y-1 
            s.delete(selectionStart - 1, selectionEnd); 
            int tempSelection = selectionEnd; 
            //这里我修改了原作者 不需要这部
            //source_des.setText(s); 
            //如果你setText 传入s 的话会将编辑框的光标移到文本框最前面 所以这里我也注释了原作者
            //source_des.setSelection(tempSelection); 
        } 
    } 
});
  • 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

补充部分

好了大家看到了增加文本动态监听 那么我们看看删除会怎么触发事件
实验代码:

  editText.addTextChangedListener(new TextWatcher() {
            private CharSequence temp;
            private int selectionStart;
            private int selectionEnd;

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                Log.d(TAG, "onTextChanged 被执行---->s=" + s + "----start="+ start
                        + "----before="+before + "----count" +count); temp = s;
                Log.e(TAG, "onTextChanged--getSelectionStart: " + editText.getSelectionStart());
                Log.e(TAG, "onTextChanged---getSelectionEnd: " + editText.getSelectionEnd());
            }

            public void beforeTextChanged(CharSequence s, int start, int count,int after) {
                Log.d(TAG, "beforeTextChanged 被执行----> s=" + s+"----start="+ start
                        + "----after="+after + "----count" +count);
                Log.e(TAG, "beforeTextChanged---getSelectionStart: " + editText.getSelectionStart());
                Log.e(TAG, "beforeTextChanged---getSelectionEnd: " + editText.getSelectionEnd());
            }

            public void afterTextChanged(Editable s) {
                Log.d(TAG, "afterTextChanged 被执行---->" + s);
                selectionStart = editText.getSelectionStart();
                selectionEnd = editText.getSelectionEnd();
                Log.e(TAG, "afterTextChanged---getSelectionStart: " + editText.getSelectionStart());
                Log.e(TAG, "afterTextChanged---getSelectionEnd: " + editText.getSelectionEnd());

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

我们这里输入LOVE然后删除’ov’两个字母看看会发生
动态图显示步骤:
这里写图片描述

可以看到我把光标放入ov外面 那么光标开始位置为1 ,结束位置为3.然后删除

我们看看日志

12-03 12:20:22.355 21082-21082/a.fmy.com.test D/FMY: beforeTextChanged 被执行----> s=love----start=1----after=0----count2
12-03 12:20:22.355 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionStart: 1
12-03 12:20:22.355 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionEnd: 3
12-03 12:20:22.359 21082-21082/a.fmy.com.test D/FMY: onTextChanged 被执行---->s=le----start=1----before=2----count0
12-03 12:20:22.359 21082-21082/a.fmy.com.test E/FMY: onTextChanged--getSelectionStart: 1
12-03 12:20:22.359 21082-21082/a.fmy.com.test E/FMY: onTextChanged---getSelectionEnd: 1
12-03 12:20:22.412 21082-21082/a.fmy.com.test D/FMY: afterTextChanged 被执行---->le
12-03 12:20:22.412 21082-21082/a.fmy.com.test E/FMY: afterTextChanged---getSelectionStart: 1
12-03 12:20:22.412 21082-21082/a.fmy.com.test E/FMY: afterTextChanged---getSelectionEnd: 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里我放一张有颜色区分图片 (和上面一样的 只不过方便大家查看)
这里写图片描述

我们先来看看beforeTextChanged的日志部分

12-03 12:20:22.355 21082-21082/a.fmy.com.test D/FMY: beforeTextChanged 被执行----> s=love----start=1----after=0----count2
12-03 12:20:22.355 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionStart: 1
12-03 12:20:22.355 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionEnd: 3
  • 1
  • 2
  • 3

s:文字没有改变前字符串
start:准备要变化文本的位置下标 ,我们这里选择’ov’位置 所以这里相对应’love’位置为1
count:相对没改变前旧文本文本减少数量 因为我们这里要删除’ov’所以为2
after:新文本新加入的字符数量 这里没有增加反而减少 所以为0

getSelectionStart:我们光标的位置不是o的左边吗?所以为1
getSelectionEnd:光标位置在v右边 所以为3 大家回去看下动态图


我们最后来看一下另外onTextChanged
这部分的日志如下:

12-03 12:20:22.359 21082-21082/a.fmy.com.test D/FMY: onTextChanged 被执行---->s=le----start=1----before=2----count0
12-03 12:20:22.359 21082-21082/a.fmy.com.test E/FMY: onTextChanged--getSelectionStart: 1
12-03 12:20:22.359 21082-21082/a.fmy.com.test E/FMY: onTextChanged---getSelectionEnd: 1
  • 1
  • 2
  • 3

s:被改变后的文本 因为我们这里删除删除’ov’ 所以为le
start:文本开始改变的位置 ‘ov’相对原本文本的开始位置1,所以这里返回1
before:改变之前旧文本减少的数量 这里 ‘love’减少 ‘ov’相当于减少了2
count:新文本添加数量 这里是减少2所以返回0

getSelectionStart 这里删除后的光标状态 所以等于1
getSelectionEnd 这里删除后的光标状态 所以开始坐标等结束坐标 因此等于1

补充部分2

我们假设剪切板内容’12’ (意思是说我们赋值了12字符串在剪切板,只要一粘贴就会出现’12’)

那么我们做一个实验 在love上 用光标选择’ov’ 然后粘贴’12’
动态图(大家耐心等下):
这里写图片描述

这里日志为:

12-03 12:51:25.347 21082-21082/a.fmy.com.test D/FMY: beforeTextChanged 被执行----> s=love----start=1----after=2----count2
12-03 12:51:25.347 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionStart: 3
12-03 12:51:25.347 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionEnd: 3
12-03 12:51:25.348 21082-21082/a.fmy.com.test D/FMY: onTextChanged 被执行---->s=l12e----start=1----before=2----count2
12-03 12:51:25.348 21082-21082/a.fmy.com.test E/FMY: onTextChanged--getSelectionStart: 3
12-03 12:51:25.348 21082-21082/a.fmy.com.test E/FMY: onTextChanged---getSelectionEnd: 3
12-03 12:51:25.378 21082-21082/a.fmy.com.test D/FMY: afterTextChanged 被执行---->l12e
12-03 12:51:25.378 21082-21082/a.fmy.com.test E/FMY: afterTextChanged---getSelectionStart: 3
12-03 12:51:25.379 21082-21082/a.fmy.com.test E/FMY: afterTextChanged---getSelectionEnd: 3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里我就简单说下光标位置的问题:因为替换相同长度的文本 所以光标并没有移动 位于选择的字符串+1的 位置 ,我们这里选择’ov’ 所以开始和结束为:3 (o的位置)

beforeTextChanged 日志解释:

12-03 12:51:25.347 21082-21082/a.fmy.com.test D/FMY: beforeTextChanged 被执行----> s=love----start=1----after=2----count2
12-03 12:51:25.347 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionStart: 3
12-03 12:51:25.347 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionEnd: 3
  • 1
  • 2
  • 3

start:文本开始位置,因为我们从’ov’的’o’开始改变所以为1(love中o不是相对是1嘛)
after:新文本增加的数量 因为增加了12所以两个字符就是2
count:原本旧字符串减少的数量 减少’ov’所以是2
其他的同学们可以自己推断

补充部分3

这里我们再看看替换不同长度的文本

我们这里 光标选择’ov’ 替换为’12345’(剪切板以保存)

这里写图片描述

12-03 13:01:25.735 21082-21082/a.fmy.com.test D/FMY: beforeTextChanged 被执行----> s=love----start=1----after=5----count2
12-03 13:01:25.735 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionStart: 3
12-03 13:01:25.735 21082-21082/a.fmy.com.test E/FMY: beforeTextChanged---getSelectionEnd: 3
12-03 13:01:25.737 21082-21082/a.fmy.com.test D/FMY: onTextChanged 被执行---->s=l12345e----start=1----before=2----count5
12-03 13:01:25.737 21082-21082/a.fmy.com.test E/FMY: onTextChanged--getSelectionStart: 6
12-03 13:01:25.737 21082-21082/a.fmy.com.test E/FMY: onTextChanged---getSelectionEnd: 6
12-03 13:01:25.770 21082-21082/a.fmy.com.test D/FMY: afterTextChanged 被执行---->l12345e
12-03 13:01:25.770 21082-21082/a.fmy.com.test E/FMY: afterTextChanged---getSelectionStart: 6
12-03 13:01:25.770 21082-21082/a.fmy.com.test E/FMY: afterTextChanged---getSelectionEnd: 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以 替换字符串的时候光标 开始等于结束的

补充部分4 Editable

我们想直接操作文本编辑框的文本的时候 想快速插入和添加 可以考虑这个方法

我们看看两个例子 获取 文本编辑框中的字符串并且添加 ‘你好’在后面;
不使用Editable

 String s = editText.getText().toString();
        s +="你好";
        editText.setText(s);
  • 1
  • 2
  • 3

来我们看看使用Editable

 editText.getText().append("你好");
  • 1
  1. 我们看看怎么获取Editable
    非常简单只需要用文本编辑框调用getText()方法

     Editable text = editText.getText();
    • 1
  2. 相关API
    在文本编辑框后面添加字符串

     Editable editable = editText.getText();
            editable.append("你好");
    • 1
    • 2

    删除文本编辑框部分内容,假设你此时文本编辑框的内容’love’你想删除中间的ov

      Editable editable = editText.getText();
            //start为要删除文本的开始下标 end为结束下标(不包括)
            //也就是说 [start,end)
            //editable.delete(start,end);
            //注意end必须等于start 不然奔溃
            // 也就是 end>=start
            //我们看看删除love 中的ov
            editable.delete(1,3);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在文本编辑框中字符串的某个部分插入字符,假设我们的文本编辑框内容为’love’那么我们想插入 ‘a’到’o’后面也就是’loave’

 Editable editable = editText.getText();
        String a = "a";
        //love 插入o后面 o位置相对于字符的1
        //第一个参数 插入 的位置 
        // 第二个参数 要插入字符串
        // 第三个参数 插入的字符串的开始位置
        // 第四个参数 插入的字符串的结束位置(不包含)
        editable.insert(1,"a",0,a.length());
       // 下面方法和上面的等价
        //editable.insert(1,"a");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
删除文本编辑框所有内容
  • 1
  • 2
  Editable editable = editText.getText();
        editable.clear();
  • 1
  • 2

替换文本编辑框部分内容
假设我们将文本框 ‘love’中”o”替换为”a” 也就是说’lave’

 Editable editable = editText.getText();
        //第一个参数 替换位置
        //第二个  替换结束为止(不包含)
        //第三个 替换的字符串
        editable.replace(1,2,"a");

        String a = "a";
        //第一个参数 替换位置
        //第二个  替换结束为止(不包含)
        //第三个 替换的字符串
        //第四个 替换文本的开始位置
        //第五个 替换文本结束位置 不包含
        editable.replace(1,2,"a",0,a.length());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Android 程序保活,锁机代码

前言

保活:如何让我们的app在Android系统不被杀死 保证存活,简单做法就是提升程序的优先级,看完本文一些流氓锁机你也会了哦.但锁机源码我不打算提供 为了防止某些恶心的人直接复制然后在市面上搞破坏

android 进程优先级如下:
1. 前台进程;Foreground process

1. 用户正在交互的Activity(onResume())
2. 当某个Service绑定正在交互的Activity。
3. 被主动调用为前台Service(startForeground())
4. 组件正在执行生命周期的回调(onCreate()/onStart()/onDestroy())
5. BroadcastReceiver 正在执行onReceive();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2. 可见进程;Visible process

1. 我们的Activity处在onPause()(没有进入onStop())
2. 绑定到可见的Activity的Service。
  • 1
  • 2
  • 3

3. 服务进程;Service process

1. 简单的startService()启动。
  • 1
  • 2

4. 后台进程;Background process

1. 对用户没有直接影响的进程----Activity出于onStop()的时候。
  • 1
  • 2

5. 空进程; Empty process

1. 不含有任何的活动的组件。(android设计的,为了第二次启动更快,采取的一个权衡)
  • 1
  • 2

如何查看系统进程的优先级状况

前提:需要root
本人在window环境下做示范

  1. 打开控制台
  2. adb shell (进入android命令控制)
  3. su (得到root权限)
  4. cat /proc/${pid}/oom_adj (${pid}指代进程pid)
    • 然后控制台会输出一个数字
      这里写图片描述

数字对应含义如下
主要有这么几个优先级(oom_adj值) 转载至参考文献:

  • UNKNOWN_ADJ = 16
    预留的最低级别,一般对于缓存的进程才有可能设置成这个级别

Adjustment used in certain places where we don’t know it yet. (Generally this is something that is going to be cached, but we don’t know the exact value in the cached range to assign yet.)

  • CACHED_APP_MAX_ADJ = 15
    缓存进程,空进程,在内存不足的情况下就会优先被kill

This is a process only hosting activities that are not visible, so it can be killed without any disruption.

  • CACHED_APP_MIN_ADJ = 9
    缓存进程,也就是空进程

  • SERVICE_B_ADJ = 8
    不活跃的进程

The B list of SERVICE_ADJ – these are the old and decrepit services that aren’t as shiny and interesting as the ones in the A list.

  • PREVIOUS_APP_ADJ = 7
    切换进程

This is the process of the previous application that the user was in. This process is kept above other things, because it is very common to switch back to the previous app. This is important both for recent task switch (toggling between the two top recent apps) as well as normal UI flow such as clicking on a URI in the e-mail app to view in the browser, and then pressing back to return to e-mail.

  • HOME_APP_ADJ = 6
    与Home交互的进程

This is a process holding the home application – we want to try avoiding killing it, even if it would normally be in the background, because the user interacts with it so much.

  • SERVICE_ADJ = 5
    有Service的进程

This is a process holding an application service – killing it will not have much of an impact as far as the user is concerned.

  • HEAVY_WEIGHT_APP_ADJ = 4
    高权重进程

This is a process with a heavy-weight application. It is in the background, but we want to try to avoid killing it. Value set in system/rootdir/init.rc on startup.

  • BACKUP_APP_ADJ = 3
    正在备份的进程

This is a process currently hosting a backup operation. Killing it is not entirely fatal but is generally a bad idea.

  • PERCEPTIBLE_APP_ADJ = 2

    可感知的进程,比如那种播放音乐

This is a process only hosting components that are perceptible to the user, and we really want to avoid killing them, but they are not immediately visible. An example is background music playback.

  • VISIBLE_APP_ADJ = 1
    可见进程

This is a process only hosting activities that are visible to the user, so we’d prefer they don’t disappear.
– FOREGROUND_APP_ADJ = 0
前台进程
This is the process running the current foreground app. We’d really rather not kill it!

  • PERSISTENT_SERVICE_ADJ = -11
    重要进程

This is a process that the system or a persistent process has bound to, and indicated it is important.

  • PERSISTENT_PROC_ADJ = -12
    核心进程

This is a system persistent process, such as telephony. Definitely don’t want to kill it, but doing so is not completely fatal.

  • SYSTEM_ADJ = -16
    系统进程

The system process runs at the default adjustment.

  • NATIVE_ADJ = -17
    系统起的Native进程

Special code for native processes that are not being managed by the system (so don’t have an oom adj assigned by the system).

在Android-18及以下空进程不叫CACHED_APP_MIN_ADJ ,叫HIDDEN_APP_MIN_ADJ,有这么点不一样,但是值都一样。

如何查看系统进程的PID

本人在window环境下做示范

  1. 打开控制台
  2. adb shell (进入android命令控制)
  3. ps

此时你会看到控制台输出很多信息 如何你需要查找你的app进程pid比较麻烦的会可以把输出信息输出到文本 ,然后在文本中查找包名

adb shell ps > print.txt (输出到print.txt文本中)

  • 参考图
    这里写图片描述

  • 对应文本位于dosSave下(仅限本例,放的位置是根据你命令行所在文件夹位置)
    这里写图片描述

流氓技术之旅

1. 锁屏时开启一个前台activity让程序变为前台进程

  • 大致思路
    1. 开启一个广播接收锁屏,和点亮屏幕广播
    2. 锁屏时开启1个1像素透明的activity(目的让用户看不见)
    3. 点亮时销毁

tip:由于监听锁屏和点亮广播在高版本 无法静态注册(无法再清单文件中写一个广播)

  • 创建一个Activity 让广播接收锁屏广播后开启一个一像素窗口,
    注意一定要继承一个含有
<item name="android:windowBackground">@android:color/transparent</item>的主题 不然会显示一片黑色的东西
  • 1

来看看具体代码:

public class EmptyActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //这里注意一定要activity在清单继承一个含有
        // <item name="android:windowBackground">@android:color/transparent</item>主题
        // 不然会不会透明 显示一片黑色

        //大家待会往下看就知道为什么
        MyApp application = (MyApp) getApplication();
        //注册一个引用 方便 解锁的时候销毁让广播类销毁
        application.putActivity(this);

        //我们的activity是绘制在window之下的 所以我们直接修改window参数

        //window显示的时候位于锁屏界面之下
        Window window = getWindow();
        //放置在窗口左边
        window.setGravity(Gravity.LEFT);
        //得到布局参数
        WindowManager.LayoutParams attributes = window.getAttributes();
        attributes.height = 1;//设置窗口大小 为1
        attributes.width =1;
        attributes.x = 0;//位置设置
        attributes.y = 0;
        //设置参数
        window.setAttributes(attributes);
    }
}
  • 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

主题样式

  <style name="MyStyle" parent="Theme.AppCompat">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:backgroundDimEnabled">false</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowAnimationStyle">@null</item>
        <item name="android:windowDisablePreview">true</item>
        <item name="android:windowNoDisplay">false</item>
    </style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • Application 类 Myapp
/**
 * Created by FMY on 2017/4/2.
 */

public class MyApp extends Application {

     WeakReference<Activity> weakReference;

    public void putActivity(Activity activity){

        weakReference = new WeakReference<>(activity);

    }
    public  void finishActivity(){


        if (weakReference != null) {
            Activity activity1 = weakReference.get();
            if (activity1 != null) {
                activity1.finish();
            }
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //注册接收锁屏和点亮屏幕广播接收者
        IntentFilter intentFilter = new IntentFilter();

        intentFilter.addAction(Intent.ACTION_SCREEN_ON);

        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
        intentFilter.addAction(Intent.ACTION_USER_PRESENT);

        intentFilter.setPriority(10000);
        registerReceiver(new OffSreenBroadcast(), intentFilter);
    }

}
  • 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
  • 最后再来看看广播接收者类OffSreenBroadcast
public class OffSreenBroadcast extends BroadcastReceiver {

    private static final String TAG = "OffSreenBroadcast";

    @Override
    public void onReceive(Context context, Intent intent) {

        if (intent==null||intent.getAction()==null) {
            return;
        }
        if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
            Log.e(TAG, "屏幕关了启动屏幕 开启一个activity: " +intent.getAction() );

            Intent intent1 = new Intent(context, EmptyActivity.class);
            intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent1);
        }

        if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
            Log.e(TAG, "屏幕开了 销毁" +intent.getAction() );
            MyApp app = (MyApp) context;
            app.finishActivity();
        }
    }
}
  • 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

2. 双进程守护

  1. 开启两个c进程守护 因为5.0以上卸载或者关闭程序会同一uid的进程一起关掉所以我这里就不做此方法阐述(需要JNI/NDK基本知识)
  2. 开启双服务 同样关闭程序的时候会把两个服务杀死 但是简单所以我们这里就介绍此种方式
  • tip:

    1. 双服务可以防止一些鲁大师应用的杀死,但无法避免用户直接按下最近任务按键,然后清理所有程序杀死.
    2. 一般情况下服务(startServeice)和宿主进程在一个进程 除非另行指定
    3. 指定服务和宿主进程不在同一进程方法如下:在清单文件中服务添加android:process=":aa"属性
      这里写图片描述
    4. 如果服务和宿主进程在同一进程 那么宿主进程发生奔溃现象服务将一起销毁(系统核心进程除外 放入system/app的应用 对应oom_adj=-12)
  • 大致思路:开启两个服务相互aidl互相绑定,如果其中一个被杀死会回调另一个服务的onServiceDisconnected 此时重新绑定即可

两个服务代码:
1. OneService :

public class OneService extends Service {
    public OneService() {

    }

    private static final String TAG = "OneService";
    @Override
    public void onCreate() {
        super.onCreate();

    }

    class MyConnet implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

            new Thread(){
                @Override
                public void run() {
                    super.run();
                    while (true) {
                        try {
                            sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        Log.e(TAG, "onServiceConnected: ");
                    }
                }
            }.start();

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "onServiceDisconnected: ");
            Intent intent1 = new Intent(OneService.this,TwoService.class);
            //BIND_IMPORTANT 表示这个服务非常重要 提高优先级
            bindService(intent1,new OneService.MyConnet(), Context.BIND_AUTO_CREATE);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand: " );
        Intent intent1 = new Intent(this,TwoService.class);
        //BIND_IMPORTANT 表示这个服务非常重要 提高优先级
        bindService(intent1,new MyConnet(), Context.BIND_IMPORTANT);

        //开启通知栏变为前台
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

        builder.setContentTitle("前台服务开启中");
        builder.setContentTitle("one服务");
        builder.setSmallIcon(R.mipmap.ic_launcher);

        //开启前台
        startForeground(13,builder.build());
        //被杀掉后重启
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "onBind: " );
        return new MyIBinder();

    }

    class MyIBinder extends IMyAidlInterface.Stub
    {
        @Override
        public void basicTypes() throws 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
  1. TwoService:

public class TwoService extends Service {
    private static final String TAG = "Two Server";

    public TwoService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }

    class MyConnet implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(TAG, "onServiceConnected: " );
            new Thread(){
                @Override
                public void run() {
                    super.run();
                    while (true) {
                        try {
                            sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        Log.e(TAG, "onServiceConnected: ");
                    }
                }
            }.start();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "onServiceDisconnected: " );
            Intent intent1 = new Intent(TwoService.this, OneService.class);
            //BIND_IMPORTANT 表示这个服务非常重要 提高优先级
            bindService(intent1,new MyConnet(), Context.BIND_IMPORTANT);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand: " );
        Intent intent1 = new Intent(this,OneService.class);
        //BIND_IMPORTANT 表示这个服务非常重要 提高优先级
        bindService(intent1,new MyConnet(), Context.BIND_AUTO_CREATE);
        //开启通知栏变为前台
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

        builder.setContentTitle("前台服务开启中");
        builder.setContentTitle("one服务");
        builder.setSmallIcon(R.mipmap.ic_launcher);

        //开启前台
        startForeground(13,builder.build());

        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "onBind: " );
        return new MyIBinder();

    }

    class MyIBinder extends IMyAidlInterface.Stub
    {
        @Override
        public void basicTypes() throws 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

MainActivity :



public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startService(new Intent(this,OneService.class));
        startService(new Intent(this,TwoService.class));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

清单文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.youjiayou.fmy.maputil">

    <permission android:name="android.permission.STATUS_BAR" />

    <uses-permission android:name="android.permission.STATUS_BAR" />
    <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.REORDER_TASKS" />
    <uses-permission android:name="android.permission.REAL_GET_TASKS" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"

        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <receiver android:name=".BootBroadcastReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />

                <category android:name="android.intent.category.HOME" />

                <action android:name="android.media.AUDIO_BECOMING_NOISY" />
                <action android:name="android.intent.action.TEST_GKP" />
            </intent-filter>
        </receiver>

        <service
            android:name=".MyService"
            android:enabled="true"

            android:process=":oneServer"
            android:exported="true" >

        </service>
        <service
            android:process=":twoServer"
            android:name=".LanchService"
            android:enabled="true"
            android:exported="true">

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

3. 悬浮窗

在MIUI8 中我在服务中开启了一个悬浮窗,然后我发现一个有趣的现象,按下手机的最近任务键,然后点击清理所有任务按钮,发现服务和悬浮窗没有死,这我不惊讶,惊讶的是我在最近任务手动滑动当前进程去关掉,悬浮窗和服务才死去.也就说说我们可以学习qq黑科技在屏幕上留一个像素点悬浮窗.然后你懂的

import static android.content.ContentValues.TAG;

public class MyService extends Service {
    public MyService() {

    }

    @Override
    public void onCreate() {
        super.onCreate();

        /**
         *  悬浮窗会隐式持有此activity
         */

        WindowManager   systemService = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        //设置不透明
        layoutParams.format = PixelFormat.RGBA_8888;
        //设置悬浮窗没有触摸反馈 让用户不明白悬浮窗这回事
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

        //宽度
        layoutParams.width =1;
        //高度
        layoutParams.height = 1;


        //设置window type
        layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;

        View inflate = LayoutInflater.from(this).inflate(R.layout.hover_view, null);

        inflate.findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("fmy", "悬浮窗被点击了");
                Toast.makeText(MyService.this, "阿斯达斯", Toast.LENGTH_LONG).show();
            }
        });

        systemService.addView(inflate, layoutParams);
         new Thread(){
            @Override
            public void run() {
                super.run();
                while (true) {
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.e(TAG, "run: " );
                }

            }
        }.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}
  • 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

杀死进程复活方法

前面我们说了点击最最近任务按钮然后 清理所有任务就可以杀死所有相关服务.但是在android5.0中有一个新API却提供了便利JobScheduler.
JobScheduler是一把双刃剑可以省电也可以利用其保活,类似AlarManager.
我们使用JobScheduler创建一个周期任务,间隔10毫秒执行任务.(实际不可能 ,必须和手机的心率对齐,执行任务必须在窗口期执行,添加的队列存在android系统中,正因如此所以….)

JobSchedulerService2 :

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerService2 extends JobService {

    @Override
    public boolean onStartJob(JobParameters params) {
        Toast.makeText(this,"asd",Toast.LENGTH_LONG).show();

        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
public class MainActivity extends AppCompatActivity {


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);

        JobInfo.Builder builder2= new JobInfo.Builder( 1,
                new ComponentName( getPackageName(),
                        JobSchedulerService2.class.getName() ) );
        builder2.setPeriodic(10);
        //Persisted 重启后继续
//        builder2.setPersisted(true);
        jobScheduler.schedule(builder2.build());


    }


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

清单文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hover">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        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>


        <service
            android:name=".JobSchedulerService2"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE">

        </service>
    </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
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

上面的代码执行后哪怕杀死进程也会执行

写入系统app命令

就是把apk放入 system/app中即可
下面大家简单即可

 public void onclick(View view) {

        DataOutputStream dataOutputStream = null;


        BufferedReader err = null;

        try {

            //得到su权限
            Process process = Runtime.getRuntime().exec("su root");
            //输出命令
            dataOutputStream = new DataOutputStream(process.getOutputStream());
            //获取外部sd路径
            File externalStorageDirectory = Environment.getExternalStorageDirectory();
            //得到apk文件路径
            File apkFile = new File(externalStorageDirectory.getAbsoluteFile()+File.separator+"youjiayou.apk");
            //移动文件命令
            String command = "cp  " + apkFile.getAbsolutePath() + " system/app\n";
            //挂载为system可写 不然无法写入文件 = = 这些坑我是一点点 踩过的
            dataOutputStream.writeBytes("mount -o remount rw /system\n");
            //输出命令
            dataOutputStream.write(command.getBytes("UTF-8"));
            //挂载为可写不然无法生效
            dataOutputStream.writeBytes("chmod 777 /system/app/youjiayou.apk\n");
            //挂载为可读
            dataOutputStream.writeBytes("mount -o remount ro /system\n");

            //刷新输出
            dataOutputStream.flush();
            //重启
            dataOutputStream.writeBytes("reboot\n");
            //退出
            dataOutputStream.writeBytes("exit\n");
            //刷新输出
            dataOutputStream.flush();
            //等候命令结束
            process.waitFor();

            String line;

            String msg = "";

            //读取控制台返回的数据
            err = new BufferedReader(new InputStreamReader(process.getInputStream()));

            //读取数据
            while ((line = err.readLine()) != null) {
                msg += line;
            }

            Log.e("Fmy", "结果 " +msg);

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

            Log.e("Fmy", "发生异常" + e.getMessage());
        } finally {
            try {
                if (dataOutputStream != null) {
                    dataOutputStream.close();
                }

                if (err != null) {
                    err.close();
                }

            } catch (IOException 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
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

开机启动

开启自启需要用户手动去授权,但是在miui系统中接收到开机广播在开机几十秒后.在这里大家可以监听
<action android:name="android.media.AUDIO_BECOMING_NOISY" /> 这里当你开机和拔掉手机耳机的时候会发出广播

锁机代码大致思路

让用户手动开启悬浮窗权限.WindowManager.LayoutParams. type=xxxx就可以让悬浮窗在整个android系统视图最上层. = =当然大家千万别再给开机权限了不然更恶心

示例apk:正在制作稍后上传

参考文献

文献1

文献2

文献3

最全的增量更新入门 包含linux端和Android

简介

增量更新大量用于 Android各大应用市场.本文想做网络上从服务器到app客户端完整讲解.app用eclipse和android studio 最新版cmark开发ndk
如下图:
这里写图片描述

以前一直好奇怎么做的直到知道了bsdiff库.
地址附上:
bsdiff源码地址和简介

  • 大家可以从简介看到bsdiff是基于bzip2源码(bsdiff和bspatch一个用于生成差异文件补丁,另一个用于差异文件和旧文件合成新文件)
    这里写图片描述
  • 下载地址说明
    这里写图片描述

应用市场原理说明

假设你用的是”XXX市场”点击更新的时候,把当前版本号和服务器最新版app版本号对比用bsdiff生成差异包,然后将差异包返回下载,下载后将本地应用app的apk文件和差异包用bspatch合成新的apk在安装.

这里写图片描述

在服务器上用java后台生成差异文件

  • 操作系统:ubuntu
  • 集成开发环境(ide):eclipse
  • 源码:bzip2 ,bsdiff

bsdiff源码点击官网中”here”下载 :
懒人链接
bzip2源码下载:
bzip2官网链接
bzip2下载链接

下载后bsdiff源码文件如下:
这里写图片描述

  • 我们看到有两个 文件一个bsdiff.c 和bspatch.c 其他文件,大家先无视(makefie可以简单理解文件工程管理的,这里可用可不用).
  • 我们服务器要生成差异文件,那么只需要bsdiff.c 即可.

  • 我们先简单阅读下bsdiff源码(我们直接看启动方法main):
    这里写图片描述
    从上图可知 传入的参数args需要等于4 不然报错.各个参数已经在上图解释

  • 由于main方法很特殊(启动方法,和java的main意义一样所以我们改一个方法名) 所以在这里我们改一个名字为 bsdiff_main
    这里写图片描述

  • 编写对应java的jni方法并生成头文件.(如果大家对jni不熟悉的,就按照本博客操作即可,尽量做到让大家都明白)

1 创建普通的java工程(当然这里不创建web工程,大家到时候自己移植到你的serverlet即可)

这里写图片描述

2 编写jni方法

package com.fmy;

public class JAVABsdiff {


    public static void main(String[] args) {



    }
    //jni方法 用于调用bsdiff
    public static native void myBsdiff(String []args);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3 生成对应”xxx.h”文件

1. 打开命令窗口 
2. 打开目录到java工程目录到src下
3. 输入javah 包名.类名 生成对应 "xxx.h"头文件
  • 1
  • 2
  • 3
  • 4

这里写图片描述
此时会在你java到src目录生成xx.h文件(请按F5刷新)
这里写图片描述
我们顺便打开这个文件看看
这里写图片描述
可以看到里面有一个c语言方法

        JNIEXPORT void JNICALL Java_com_fmy_JAVABsdiff_myBsdiff
          (JNIEnv *, jclass, jobjectArray);
  • 1
  • 2
        我们待会创建一个c文件实现它并将它与bsdiff_main方法关联
  • 1
  • 2

4 创建bsdiff.h。创建一个test.c文件 导入bsdiff.h和xxx.h 并实现

  • 导入bsdiff.h并声明bsdiff_main()方法作用是用于告诉 test.c有这个方式到实现 并且可以调用
  • xxx.h是我们前面调用javah 生成到头文件
#include "com_fmy_JAVABsdiff.h"
#include<stdio.h>
#include"bsdiff.h"

JNIEXPORT void JNICALL Java_com_fmy_JAVABsdiff_myBsdiff
(JNIEnv * env, jclass jclas,jobjectArray attas){

         //GetObjectArrayElement得到到是jstring类型 而我们调用bsdiff_main()传入的
         //是char× 数组 所以需要转化。这里需要jni知识 所以就不太多了,你只需把下面到内容
         //复制到你到项目中即可

         //旧文件地址 
         jstring a0 = (*env)->GetObjectArrayElement(env,attas,0);
         //转化为char ×
         char *j=(char*)(*env)->GetStringUTFChars(env,a0,NULL);

         //
         jstring a1 = (*env)->GetObjectArrayElement(env,attas,1);
         char *j1=(char*)(*env)->GetStringUTFChars(env,a1,NULL);

         jstring a2 = (*env)->GetObjectArrayElement(env,attas,2);
         char *j2=(char*)(*env)->GetStringUTFChars(env,a2,NULL);


         char * agrs[] = {"patch",j,j1,j2};


         bsdiff_main(4,agrs);
}
  • 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

5 编译生成方式1 gcc命令

  • 把上面所有到文件放入到一个文件中
    1. bsdiff.h
    2. bsdiff.c
    3. test.c
    4. xxx.h
    5. bzip2文件下所有源码

这里写图片描述

打开命令窗口 打开此目录输入

$ gcc *.c -fPIC -shared -o libtest.so
  • 1

然后报错
这里写图片描述
为什么报错?因为 前面用javah生成头文件中导入了jni.h 这里大家可以回头看看。第二我们使用env这个变量 也是jni里面所有我们需要导入。

问题来了jni.h在哪?
位于java安装目录include下
这里写图片描述

ok我们把这个文件复制到刚才编译目录下,继续编译
这里写图片描述

又报错发现缺少jni_md.h 文件
报错原因:因为jni.h内部引用了jni_md.h

jni_md.h在哪?
在java安装目录到include到linux下
这里写图片描述

我们在最后编译下
这里写图片描述

发现还是报错xxx.c中定义main重复定义了
大家这里可以看着报错到文件 去到文件直接把main方法删除了或注释
这里我就带大家注释其中到bzip2recover.c的main
这里写图片描述
删除或则注释上面高亮部分
其他文件大家自己删除把

再次编译一次 快疯了。。。。。
编译通过了!
这里写图片描述

此时在目录下会生成test.so

在java调用so

package com.fmy;

public class JAVABsdiff {

    static{
        System.load("/media/fmy/新加卷/增量/完整/test.so");
    }

    public static void main(String[] args) {

        String oldfile = "/media/fmy/新加卷/增量/old.tar";

        String newfile = "/media/fmy/新加卷/增量/new.tar";

        String patch = "/media/fmy/新加卷/增量/patch.patch";


        myBsdiff(new String[]{oldfile,newfile,patch});

        System.out.println("asd");

    }
    //生成静态到patch文件
    public static native void myBsdiff(String []args);
}
  • 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

5 编译生成方式2 eclipse

elipse 需要安装cdt插件

  1. 新建C project。

    1. 选择project type —>>Shared Libary
    2. toocahins 选择Linux GCC
    3. finish
      这里写图片描述
  2. 导入第一中编译方式 最后一步的所有文件到工程中
    这里写图片描述

  3. 修改编译参数
    1. 右键项目 在弹出到菜单栏中选择Propertice
    2. c/c++ Build –>>Setings 在右侧窗的command输入gcc -fPIC
      这里写图片描述
    3. build一下在debug下就可以看到对应的so类库

eclipse cdt编译参考文献1
eclipse cdt编译参考文献2

Android eclipse实现patch

  1. eclipse 新建一个Android 工程
  2. 右键选择Android Tools—>>Add Native Support 点击确定
    这里写图片描述

  3. 此时会在目录生成 生成一个lib目录 并且自动生成一个.cpp和Android.mk文件

  4. 创建jni方法

    public class MainActivity extends Activity {
    
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
        }
    
    
        public static native void path(String []arrays);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  5. 生jni方法头文件放入jni中

    这里写图片描述

  6. 把去掉bzip2源码中所有的main方法的文件拷贝到jni目录下,

  7. 创建一个test.c文件放入jni目录

  8. 把 bspatch.c放入jni目录.并修改对应的main
    这里写图片描述

    这里写图片描述

    修改bspatch.c导入的bzip.h路径
    这里写图片描述

    我们顺带看jni 目录
    这里写图片描述

  9. 编写test.c实现

    
    #include "com_fmy_JAVA_bs.h"
    
    
    #include<stdio.h>
    
    
    
    JNIEXPORT void JNICALL Java_com_fmy_JAVA_1bs_myBsDiff
    (JNIEnv * env, jclass jclas,jobjectArray attas){
        jstring a0 = (*env)->GetObjectArrayElement(env,attas,0);
            char *j=(*env)->GetStringUTFChars(env,a0,NULL);
            jstring a1 = (*env)->GetObjectArrayElement(env,attas,1);
            char *j1=(*env)->GetStringUTFChars(env,a1,NULL);
            jstring a2 = (*env)->GetObjectArrayElement(env,attas,2);
            char *j2=(*env)->GetStringUTFChars(env,a2,NULL);
    
            char * agrs[] = {"patch",j,j1,j2};
    
    
            bsdiff_main(4,agrs);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  10. 修改Android.mk文件如下

    LOCAL_PATH := $(call my-dir)
    
    #目的便利jni目录下所有的.c文件
    
    MY_CPP_LIST := $(wildcard $(LOCAL_PATH)/*.c)
    
    #目的便利jni/bzip2目录下所有的.c文件
    
    MY_CPP_LIST += $(wildcard $(LOCAL_PATH)/bzip2/*.c)
    
    
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE    := bspatch
    LOCAL_SRC_FILES := $(MY_CPP_LIST:$(LOCAL_PATH)/%=%)
    
    #导入Android 日志 库可以在jni中使用Log.e方法等
    
    LOCAL_LDLIBS:= -llog
    
    include $(BUILD_SHARED_LIBRARY)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  11. 获取本地apk文件路径和服务器下载好的patch(差异文件)合成
    (这里偷懒模拟下就下)
public class MainActivity extends Activity {


    static{
        System.loadLibrary("bspatch");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //这里用zip包模拟 增量更新不止用于 apk哦亲
        File oldFile = new File(Environment.getExternalStorageDirectory(),"old.zip");
        File newFile = new File(Environment.getExternalStorageDirectory(),"new.zip");
        File patchFile = new File(Environment.getExternalStorageDirectory(),"path.patch");
        path(new String [] {oldFile.getAbsolutePath(),newFile.getAbsolutePath(),patchFile.getAbsolutePath()});
//      path();
        Log.e("test", "asd");
//      Log.e("fmy", "============"+path);
    }

//  public static native void path();
    public static native void path(String []arrays);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Android studio2.2实现patch

用CMake 构建ndk开发,这里不详细说了
CMake教程

用as创建一个工程的时候勾选 ‘include c++ support’
这里写图片描述

此时 工程目录下有个main/cpp文件夹
项目工程下会有个CMakeList.txt文件

  1. 导入bzip2(去掉文件中带有main方法的文件代码) 放入cpp文件夹中
  2. 创建一个jni方法

    
    
    public class MainActivity extends AppCompatActivity {
    
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("native-lib");
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
    
        }
    
        /**
         * A native method that is implemented by the 'native-lib' native library,
         * which is packaged with this application.
         */
    
        public static native void path(String []arrays);
    
    }
    
    • 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

    tip:在新版的studio直接鼠标方法jni方法上直接按下Alt+回车键直接生产对应实现方法哦.

    这里我们在cpp/native-lib.c实现

    
    #include <jni.h>
    
    
    #include "stdio.h"
    
    
    #include "android/log.h"
    
    
    #define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"jason",FORMAT,__VA_ARGS__)
    
    
    #define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"jason",FORMAT,__VA_ARGS__)
    
    
    
    
    #include "bzip2-1.0.6/bspatch.h"
    
    
    
    JNIEXPORT void JNICALL
    Java_com_myself_weather_testjni2_MainActivity_path(JNIEnv *env, jclass tjype, jobjectArray arrays) {
    
        jstring a0 = (*env)->GetObjectArrayElement(env,arrays,0);
        char *j=(*env)->GetStringUTFChars(env,a0,NULL);
        jstring a1 = (*env)->GetObjectArrayElement(env,arrays,1);
        char *j1=(*env)->GetStringUTFChars(env,a1,NULL);
        jstring a2 = (*env)->GetObjectArrayElement(env,arrays,2);
        char *j2=(*env)->GetStringUTFChars(env,a2,NULL);
    
        char * agrs[] = {"patch",j,j1,j2};
        mybspatch_main(4,agrs);
    
        LOGE("FMY%s",j);
        LOGE("FMY%s",j1);
        LOGE("FMY%s",j2);
    
    }
    • 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
  3. 导入bspatch.c导入cpp目录下(记得修改main方法名字和eclipse一样)

    • 此时cpp目录
      这里写图片描述
  4. 修改CmarkList.txt文件
    这里写图片描述

  5. 最后合成

package com.myself.weather.testjni2;

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

import java.io.File;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

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

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                File oldFile = new File(Environment.getExternalStorageDirectory(),"old.zip");
                File newFile = new File(Environment.getExternalStorageDirectory(),"new.zip");
                File patchFile = new File(Environment.getExternalStorageDirectory(),"path.patch");
                path(new String [] {oldFile.getAbsolutePath(),newFile.getAbsolutePath(),patchFile.getAbsolutePath()});
            }
        });
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */

    public static native void path(String []arrays);

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

安卓用ffmeg解码

本文章是用ffmeg解码封装格式(如mp4)转换为yuv420p保存到本地,本文是结合雷霄骅博客ppt和某地方学习的一个笔记(说出来等下被认为做广告就尴尬了)

封装格式

这里写图片描述

视频编码数据

将封装格式解压后可以得到压缩过的音视频等.
将压缩过的视频解压后可以得到 视频像素数据(RGB,YUV等).常见的视频压缩格式有H.264, MPEG4等…
这里写图片描述

YUV420P格式介绍

YUV是视频像素格式,在压缩视频格式解压可以得到,YUV也有很多种格式.下面举例三种.
1. Y代表亮度
2. uv代表色度

因为人对亮度的敏感远大于色度

转载地址
用三个图来直观地表示采集的方式吧,以黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量
这里写图片描述

先记住下面这段话,以后提取每个像素的YUV分量会用到。

YUV 4:4:4采样,每一个Y对应一组UV分量。
YUV 4:2:2采样,每两个Y共用一组UV分量。
YUV 4:2:0采样,每四个Y共用一组UV分量。

上面大家看最后一幅图即可.
这里写图片描述

编译ffmpeg的so库

在ffmpeg历史版本中选择一个版本下载到本地
ffmpeg历史发布版本连接

这里写图片描述

因为我们安卓是linux系统,所以我们下载到本地放入到linux系统编译后给安卓使用.

1下载压缩包放入linux并解压

下面是解压后目录
这里写图片描述

我们打开文件夹看看.
这里写图片描述

2编写脚本控制configure生成so

创建一个build_android.sh文件作为脚本

#!/bin/bash
make clean
export NDK=/usr/ndk/android-ndk-r10e
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"

./configure --target-os=linux \
--prefix=$PREFIX --arch=arm \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
  • 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

注意上面的几个地方

#你NDK存放在linux的路径,如果你没有下载那么请自行下载
export NDK=/usr/ndk/android-ndk-r10e
  • 1
  • 2
# 输出编译后的so路径 $(pwd)是当前路径
export PREFIX=$(pwd)/android/$CPU
  • 1
  • 2
#你要编译版本
export CPU=arm
  • 1
  • 2

上传到服务器ffmpeg解压目录下.
这里写图片描述

修改解压后ffmpeg目录文件权限
这里写图片描述

上面的代码会执行修改ffmpeg目录和子目录的权限为可读可写可执行(子目录也要不然有坑)

最后执行我们的脚本(大约会进行10分钟)
这里写图片描述

编译完成后会在 ffmpeg目录下的android的arm下生成两个文件
一个是include文件夹 ,另一个是lib
1. include 包含编译生成的so对应头文件
2. lib 生成的so文件

我们打开lib目录查看:
这里写图片描述

解释:
libXXX.so.YYYY,DDD
上面XXX是so 功能类名
YYYY,DDDD是版本号.这样库在安卓是无法使用的

有人又会说了,目录下面不是有libXXX.so吗?
他只是个linux的软连接,就像快捷方式一样

解决办法:
修改目录下configure文件部分内容如下
注释部分为原来内容

#注释的部分
#SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
#LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
#SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
#SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

#自己写的部分
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

修改之后重新 执行./build_android即可


Eclipse编译之旅

将上面的lib中的so文件(当然也可以将lib文件夹复制过去)拷贝到eclipse目录的jni下,将include文件夹也放入jin下
这里写图片描述

修改Android.mk文件

LOCAL_PATH := $(call my-dir)
#ffmpeg lib
include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := libavcodec-56.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avdevice
LOCAL_SRC_FILES := libavdevice-56.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := libavfilter-5.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := libavformat-56.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := libavutil-54.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := libpostproc-53.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := libswresample-1.so
#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := libswscale-3.so

#LOCAL_EXPORT_C_INCLUDES :=  $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)

LOCAL_MODULE    := DemoFFmepeg
LOCAL_SRC_FILES := DemoFFmepeg.c
LOCAL_LDLIBS+= -llog
LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale
#下面这行是导入include文件中头文件,这样做我们就可以在项目中
#直接include"libscan/xxx.h"而不是include"include/libscan/xxx.h"
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
include $(BUILD_SHARED_LIBRARY)
  • 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

我们先看看MainActivity.java 文件

package com.fmple.demoffmepeg;

import java.io.File;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;


public class MainActivity extends Activity {

    static{
        System.loadLibrary("DemoFFmepeg");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final File inputFile = new File(Environment.getExternalStorageDirectory(),"a.mov");
        final File outFile = new File(Environment.getExternalStorageDirectory(),"b.yuv");
        //由于是耗时操作 所以开了个线程
        new Thread(){
            public void run() {
                 ffmpeg(inputFile.getAbsolutePath(), outFile.getAbsolutePath());
            };
        }.start();


    }

   /**
    * 
    * @param input 视频文件的输入路径
    * @param out 把视频文件解码成yuv格式输出路径
    */
   public native void ffmpeg(String input,String out);


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

上面也没什么好说的,就是创建activity的时候调用我们的一个jni方法ffmpeg

最后看看方法的实现文件DemoFFmepeg.c
下面我完整翻译了所用到的API耗时挺久的,英语不好…

#include <jni.h>

#include<stdio.h>
#include<android/log.h>
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"

#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO," FMY",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"FMY",FORMAT,##__VA_ARGS__);
char * output_cstr;
/**
 * 这里提供了另一种方式转化为saveYUV420 本案例不用 大家可以自己体验下此方法(这个方法在网上看到过)
 */
static void saveYUV420P(unsigned char *buf,int wrap,int xsize, int ysize){
    FILE *fileOut = NULL;

    int i;
    if(buf ==NULL){
        LOGE("缓存为空");
        return ;
    }

    fileOut = fopen(output_cstr,"ab+");

    for (i = 0;i<ysize; ++i) {

        fwrite(buf+i*wrap,1,xsize,fileOut);

    }
    fflush(fileOut);
    fclose(fileOut);

}
JNIEXPORT void JNICALL Java_com_fmple_demoffmepeg_MainActivity_ffmpeg
(JNIEnv * env, jobject jobj, jstring input, jstring out){


    char * input_char = (*env)->GetStringUTFChars(env,input,NULL);

    output_cstr = (*env)->GetStringUTFChars(env,out,NULL);


    //头文件libavformat/avformat.h
    //注册所有组件
    /**
     * 初始化libavformat和注册所有的 muxers, demuxers 和协议,如果你不想使用次函数,
     * 则可以手动选择你想要的支持格式
     * 详情你可以选择下面的函数查询
     * @see av_register_input_format()
     * @see av_register_output_format()
     *
     * muxer是合并将视频文件、音频文件和字幕文件合并为某一个视频格式。如,可将a.avi, a.mp3, a.srt用muxer合并为mkv格式的视频文件。
     * demuxer是拆分这些文件的。
     */
    av_register_all();

    // 封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。
    AVFormatContext * pFormatCtx = avformat_alloc_context();

    /**
     * 打开输入流并且读取头信息。但解码器没有打开
     * 这个输入流必须使用avformat_close_input()关闭
     * @param ps(第一个参数的形参名称) 指向 你由你提供AVFormatContext(AVFormatContext是由avformat_alloc_context函数分配的)。
     * 有可能ps指向空,在这种情况下,AVFormatContext由此函数分配并写入ps。
     * 注意: 你提供的AVFormatContext在函数执行失败的时候将会被释放
     * @param url 你要打开视频文件路径.
     * @param fmt  如果不为空,那么这个参数将强制作为输入格式,否则自动检索
     * @param options 一个关于AVFormatContext and demuxer-private 选项的字典.
     * 返回时,此参数将被销毁,并替换为包含未找到的选项的dict。有可能是空的
     *
     * @return 返回0表示成功, 一个负数常量AVERROR是失败的.
     *
     * @note 如果你想自定义IO,你需要预分配格式内容并且设置pd属性
     */
    if(avformat_open_input(&pFormatCtx,input_char,NULL,NULL)!=0){
        LOGE("NDK>>>%s","avformat_open_input打开失败");
        return;
    }

    //上面打开输入流后会将视频封装格式信息写入AVFormatContext中那么我们下一步就可以得到一些展示信息
    /**
     *
     * 读取媒体文件中的数据包以获取流信息,这个对于对于文件格式没有头信息的很有帮助,比如说mpeg
     * 这个函数还可以计算在MPEG-2重复帧模式的真实帧速率。
     * 逻辑文件位置不会被这个函数改变
     * 检索过的数据包或许会缓存以供后续处理
     * @param ic  第一个参数 封装格式上下文
     * @param options
     *              如果不为空, 一个长度为 ic.nb_streams (封装格式所有流,字幕 视频 音频等) 的字典。
     *              字典中第i个成员  包含一个对应ic第i个流中对的编码器。
     *              在返回时,每个字典将会填充没有找到的选项
     * @return 如果返回>=0 代表成功, AVERROR_xxx 表示失败
     *
     * @note 这个函数 不保证能打开所有编码器,所以返回一个非空的选项是一个完全正常的行为
     *
     *
     * @todo
     *  下个版本目标无视即可
     * Let the user decide somehow what information is needed so that
     *       we do not waste time getting stuff the user does not need.
     */
    if( avformat_find_stream_info(pFormatCtx,NULL)<0){
        LOGE("NDK>>>%s","avformat_find_stream_info失败");
        return ;
    }
    LOGE("NDK>>>%s","成功");
    //  //输出视频信息
    //  LOGI("视频的文件格式:%s",pFormatCtx->iformat->name);
    //  LOGI("视频时长:%d", (pFormatCtx->duration)/1000000);

    //获取视频流的索引位置
    //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
    int v_stream_idx = -1;
    int i = 0;
    //遍历封装格式中所有流
    for (; i < pFormatCtx->nb_streams; ++i) {

        //获取视频流pFormatCtx->streams[i]
        //pFormatCtx->streams[i]->codec获取编码器
        //codec_type获取编码器类型
        //当前流等于视频 记录下标
        if (pFormatCtx->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO) {
            v_stream_idx = i;
            break;
        }
    }
    if (v_stream_idx==-1) {
        LOGE("没有找视频流")
    }else{
        LOGE("找到视频流")
    }

    //编码器上下文结构体,保存了视频(音频)编解码相关信息
    //得到视频流编码器
    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;

    //   每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。
    AVCodec *pCodec =avcodec_find_decoder(pCodecCtx->codec_id);

    //(迅雷看看,找不到解码器,临时下载一个解码器)
    if (pCodec == NULL)
    {
        LOGE("%s","找不到解码器\n");
        return;
    }else{
        LOGE("%s","找到解码器\n");
    }


    //打开解码器
    /**
     * 初始化 指定AVCodecContext去使用 给定的AVCodec
     * 在使用之前函数必须使用avcodec_alloc_context3()分配上下文。
     *
     * 以下函数 avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
     * avcodec_find_decoder() and avcodec_find_encoder() 提供了一个简便的得到一个解码器的方法
     *
     * @warning 这个函数线程不是安全的
     *
     * @note 在使用解码程序之前,始终调用此函数 (如 @ref avcodec_decode_video2()).
     * 下面是示例代码
     * @code
     * avcodec_register_all();
     * av_dict_set(&opts, "b", "2.5M", 0);
     * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
     * if (!codec)
     *     exit(1);
     *
     * context = avcodec_alloc_context3(codec);
     *
     * if (avcodec_open2(context, codec, opts) < 0)
     *     exit(1);
     * @endcode
     *
     *
     * @param avctx 要初始化的编码器
     * @param codec 用这个codec去打开给定的上下文编码器.如果 codec 不为空 那么必须
     * 事先用avcodec_alloc_context3和avcodec_get_context_defaults3传递给这个context,那么这个codec
     * 要么为NULL要么就是上面调用函数所使用的codec
     *
     * @param
     *
     * 选项填充AVCodecContext和编解码器私有选项的字典。返回时,此对象将填充未找到的选项。
     *
     * @return 返回0表示成功, 负数失败
     * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
     *      av_dict_set(), av_opt_find().
     */
    if(avcodec_open2(pCodecCtx,pCodec,NULL)==0){
        LOGE("%s","打开编码器成功\n");
    }else{
        LOGE("%s","打开编码器失败\n");
        return;
    }
    //输出视频信息
    LOGE("视频的文件格式:%s",pFormatCtx->iformat->name);
    //得到视频播放时长
    if(pFormatCtx->duration != AV_NOPTS_VALUE){
        int hours, mins, secs, us;
        int64_t duration = pFormatCtx->duration + 5000;
        secs = duration / AV_TIME_BASE;
        us = duration % AV_TIME_BASE;
        mins = secs / 60;
        secs %= 60;
        hours = mins/ 60;
        mins %= 60;
        LOGE("%02d:%02d:%02d.%02d\n", hours, mins, secs, (100 * us) / AV_TIME_BASE);

    }
    LOGE("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
    LOGE("解码器的名称:%s",pCodec->name);


    //存储一帧压缩编码数据。
    AVPacket *packet =av_malloc(sizeof(AVPacket));

    //输出转码文件地址
        FILE *fp_yuv = fopen(output_cstr,"wb+");

    //AVFrame用于存储解码后的像素数据(YUV)
    //内存分配
    AVFrame *pFrame = av_frame_alloc();

    //YUV420转码用
    AVFrame *pFrameYUV = av_frame_alloc();

    //avpicture_get_size()函数介绍:
    //
    /**
     * 如果给定存储图片的格式,那么计算给定的宽高所占用的大小
     *
     * @param pix_fmt   图片像素格式
     * @param width     图片宽
     * @param height     图片高
     * @return 返回计算的图片缓存大小或者错误情况下的负数错误代码
     *
     *
     * 这里计算缓存区的大小,但是没有分配,这里是用来后面转码使用
     */
    uint8_t *out_buffer = av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P,pCodecCtx->width,pCodecCtx->height));

    //初始化缓冲区
    /**
     * 基于指定的图片参数和提供的图片缓存区去设置图片字段
     *
     * 使用ptr所指向的图片数据缓存  填充图片属性
     *
     * 如果 ptr是空,这个函数仅填充图片行大小(linesize)的数组并且返回图片缓存请求的大小
     *
     * 要分配图片缓存并且再一次填充图片数据请使用 avpicture_alloc().
     * @param picture       要填充的图片
     * @param ptr           存储图片的数据的缓存区, or NULL
     * @param pix_fmt       图片像素格式
     * @param width         图片宽
     * @param height        图片高
     * @return 返回请求的字节大小,在错误的情况下返回负数
     *
     * @see av_image_fill_arrays()
     */
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等
    /**
     *分配和返回 SwsContext. You need it to perform
     * scaling/conversion operations using sws_scale().
     *
     * @param srcW 原始图宽
     * @param srcH 原始图高
     * @param srcFormat 原始图格式
     * @param dstW 目标图宽
     * @param dstH 不解释
     * @param dstFormat 不解释
     * @param flags 指定一个标志用于重新调整算法和选项
     *  具体参考:http://blog.csdn.net/leixiaohua1020/article/details/12029505
     * @return 一个指定分配内容的指针, 错误情况返回空
     * @note this function is to be removed after a saner alternative is
     *       written
     */
        struct SwsContext *sws_ctx =sws_getContext(pCodecCtx->width, pCodecCtx->height,pCodecCtx->pix_fmt,
                pCodecCtx->width, pCodecCtx->height,AV_PIX_FMT_YUV420P,
                SWS_BICUBIC, NULL, NULL, NULL);


    //标志位
    int got_picture, ret;

    //读取每一帧
    /**
     *返回下一帧的流
     * 此函数返回存储在文件中的内容,并且不会验证解码器有什么有效帧。
     * 函数将存储在文件中的帧进行分割 并且返回给每一个调用者。
     *
     * 函数不会删除在有效帧之间的无效数据 以便在可能解码过程中提供解码器最大的信息帮助
     * 如果 pkt->buf 是空的,那么这个对应数据包是有效的直到下一次调用av_read_frame()
     * 或者直到使用avformat_close_input().否则包无期限有效
     * 在这两种情况下 这个数据包当你不在需要的时候,你必须使用使用av_free_packet释放它
     * 对于视屏,数据包刚好只包含一帧.对于音频,如果它每一帧是一个已知固定大小的,那么他包含整数帧(如. PCM or ADPCM data)
     * 如果音频帧具有可变大小(如. MPEG audio),那么他只包含一帧
     * pkt->pts, pkt->dts and pkt->duration 始终在AVStream.time_base 单位设置正确的数值
     *(如果这个格式无法提供.那么进行猜测)
     * 如果视频格式有B帧那么pkt->pts可以是 AV_NOPTS_VALUE.如果你没有解压他的有效部分那么最好依靠pkt->dts
     *
     * @return 0表示成功, < 0 错误或者文结束
     */
    while(av_read_frame(pFormatCtx,packet)>=0){

        //一个包里有很多种类型如音频视频等 所以判断 这个包对应的流的在封装格式的下表
        //如果这个包是视频频包那么得到压缩的视频包
        if (packet->stream_index==v_stream_idx) {
            LOGE("测试");
            /**
             * 解码视频帧 从avpkt->data读取数据并且解码avpkt->size的大小后转化为图片.
             * 一些解码器可以支持在一个ACpacket中存在多帧的情况,像这样的解码器将只解码第一帧
             *
             * @warning  输入缓存区必须 实际读取的字节流小于 FF_INPUT_BUFFER_PADDING_SIZE,
             * 一些优化过的比特流 读取32位或者64字节 的时候可以一次性读取完
             *
             * @warning 在缓冲器的buf结尾设置0以确保被破坏的MPEG流不会发生超线程
             *
             * @note 有 CODEC_CAP_DELAY 才能设置一个在输入和输出之间的延迟,这些需要使用avpkt->data=NULL,
             *  在结束返回剩余帧数的时候avpkt->size=0
             *
             * @note  这个AVCodecContext 在数据包传入解码器之前必须调用avcodec_open2
             *
             *
             * @param avctx 解码器上下文
             *
             * @param[out] 解码的视频帧图片将会被存储在AVFrame.
             *                 使用av_frame_alloc 得到一个AVFrame,
             *                 编码器将会分配 使用  AVCodecContext.get_buffer2() 回调
             *                 的实际图片的内存.
             *                 当AVCodecContext.refcounted_frames 设置为1,这帧(frame)是引用计数,并且返回
             *                 的引用计数是属于调用者的.
             *             frame在长实际不使用的时候调用者必须调用av_frame_unref()就行释放
             *             如果av_frame_is_writable()返回1那么调用者可以安全的写入到这个frame中。
             *                 当AVCodecContext.refcounted_frames设置为0,返回的引用属于解码器,
             *                只有下次使用这个函数或者关闭或者刷新这个编码器之前有效。调用者不会写入它
             *
             *@param[in,out] got_picture_ptr 如果为0表示不能解压, 否者它不是0.
             *
             * @param[in] avpkt 这个输入的avpkt包含输入缓存区
             *              你能使用av_init_packet()创建像这样的packet然后设置数据和大小,
             *              一些解码器还可以添加一些其他字段 比如  flags&AV_PKT_FLAG_KEY  flags&AV_PKT_FLAG_KEY
             *          所有解码器都设计为尽可能少地使用
             *
             * @return 再错误时返回一个负数 , 否则返回使用字节数或者或者0(没有帧被解压返回0)otherwise the number of bytes
             *
             */
            ret=avcodec_decode_video2(pCodecCtx,pFrame,&got_picture,packet);
            if(ret>=0){
                LOGE("解压成功");
                //AVFrame转为像素格式YUV420,宽高
                //2 6输入、输出数据
                //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
                //4 输入数据第一列要转码的位置 从0开始
                //5 输入画面的高度
                sws_scale(sws_ctx,pFrame->data,pFrame->linesize,0,pCodecCtx->height,pFrameYUV->data,pFrameYUV->linesize);

                //输出到YUV文件
                //AVFrame像素帧写入文件
                //data解码后的图像像素数据(音频采样数据)
                //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感
                //U V 个数是Y的1/4
                int y_size = pCodecCtx->width * pCodecCtx->height;

                fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);

            }
        }
        av_free_packet(packet);
    }


    (*env)->ReleaseStringUTFChars(env,input,input_char);

    (*env)->ReleaseStringUTFChars(env,out,output_cstr);
    //关闭文件
    fclose(fp_yuv);

    //关闭资源
    av_frame_free(&pFrame);
    av_frame_free(&pFrameYUV);
    //关闭编码器上下文
    avcodec_close(pCodecCtx);
    //关闭输入流
    avformat_close_input(&pFormatCtx);
    //关闭封装格式
    avformat_free_context(pFormatCtx);
}

  • 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
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396

Android Studio编译

注意:创建项目的时候勾选Include C++ support

这里写图片描述

导入所有so库到libs中
这里写图片描述

将include头文件导入cpp目录下
这里写图片描述

修改CMakeLists.txt文件

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.


set(distribution_DIR ${CMAKE_SOURCE_DIR}/libs/)

#libavcodec-56.so
add_library(libavcodec-56-lib SHARED IMPORTED)
set_target_properties(libavcodec-56-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavcodec-56.so)


#libavdevice-56.so
add_library(libavdevice-56-lib SHARED IMPORTED)
set_target_properties(libavdevice-56-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavdevice-56.so)

#libavfilter-5.so
add_library(libavfilter-5-lib SHARED IMPORTED)
set_target_properties(libavfilter-5-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavfilter-5.so)

#libavformat-56.so
add_library(libavformat-56-lib SHARED IMPORTED)
set_target_properties(libavformat-56-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavformat-56.so)

#libavutil-54.so
add_library(libavutil-54-lib SHARED IMPORTED)
set_target_properties(libavutil-54-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavutil-54.so)


#libpostproc-53.so
add_library(libpostproc-53-lib SHARED IMPORTED)
set_target_properties(libpostproc-53-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libpostproc-53.so)


#libswresample-1.so
add_library(libswresample-1-lib SHARED IMPORTED)
set_target_properties(libswresample-1-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libswresample-1.so)


#libswscale-3.so
add_library(libswscale-3-lib SHARED IMPORTED)
set_target_properties(libswscale-3-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libswscale-3.so)


add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.c )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
include_directories( src/main/cpp/include/ )
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib}
                       libavcodec-56-lib
                       libavdevice-56-lib
                       libavfilter-5-lib
                       libavformat-56-lib
                       libavutil-54-lib
                       libpostproc-53-lib
                       libswresample-1-lib
                       libswscale-3-lib
                       )
  • 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

最后代码实现

#include <jni.h>
#include<android/log.h>
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO," FMY",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"FMY",FORMAT,##__VA_ARGS__);
JNIEXPORT void JNICALL
Java_com_example_demoffmpeg_MainActivity_ffmpeg(JNIEnv * env, jobject jobj, jstring input, jstring out) {

    char * input_char = (*env)->GetStringUTFChars(env,input,NULL);

    char *  output_cstr = (*env)->GetStringUTFChars(env,out,NULL);


    //头文件libavformat/avformat.h
    //注册所有组件
    /**
     * 初始化libavformat和注册所有的 muxers, demuxers 和协议,如果你不想使用次函数,
     * 则可以手动选择你想要的支持格式
     * 详情你可以选择下面的函数查询
     * @see av_register_input_format()
     * @see av_register_output_format()
     *
     * muxer是合并将视频文件、音频文件和字幕文件合并为某一个视频格式。如,可将a.avi, a.mp3, a.srt用muxer合并为mkv格式的视频文件。
     * demuxer是拆分这些文件的。
     */
    av_register_all();

    // 封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。
    AVFormatContext * pFormatCtx = avformat_alloc_context();

    /**
     * 打开输入流并且读取头信息。但解码器没有打开
     * 这个输入流必须使用avformat_close_input()关闭
     * @param ps(第一个参数的形参名称) 指向 你由你提供AVFormatContext(AVFormatContext是由avformat_alloc_context函数分配的)。
     * 有可能ps指向空,在这种情况下,AVFormatContext由此函数分配并写入ps。
     * 注意: 你提供的AVFormatContext在函数执行失败的时候将会被释放
     * @param url 你要打开视频文件路径.
     * @param fmt  如果不为空,那么这个参数将强制作为输入格式,否则自动检索
     * @param options 一个关于AVFormatContext and demuxer-private 选项的字典.
     * 返回时,此参数将被销毁,并替换为包含未找到的选项的dict。有可能是空的
     *
     * @return 返回0表示成功, 一个负数常量AVERROR是失败的.
     *
     * @note 如果你想自定义IO,你需要预分配格式内容并且设置pd属性
     */
    if(avformat_open_input(&pFormatCtx,input_char,NULL,NULL)!=0){
        LOGE("NDK>>>%s","avformat_open_input打开失败");
        return;
    }

    //上面打开输入流后会将视频封装格式信息写入AVFormatContext中那么我们下一步就可以得到一些展示信息
    /**
     *
     * 读取媒体文件中的数据包以获取流信息,这个对于对于文件格式没有头信息的很有帮助,比如说mpeg
     * 这个函数还可以计算在MPEG-2重复帧模式的真实帧速率。
     * 逻辑文件位置不会被这个函数改变
     * 检索过的数据包或许会缓存以供后续处理
     * @param ic  第一个参数 封装格式上下文
     * @param options
     *              如果不为空, 一个长度为 ic.nb_streams (封装格式所有流,字幕 视频 音频等) 的字典。
     *              字典中第i个成员  包含一个对应ic第i个流中对的编码器。
     *              在返回时,每个字典将会填充没有找到的选项
     * @return 如果返回>=0 代表成功, AVERROR_xxx 表示失败
     *
     * @note 这个函数 不保证能打开所有编码器,所以返回一个非空的选项是一个完全正常的行为
     *
     *
     * @todo
     *  下个版本目标无视即可
     * Let the user decide somehow what information is needed so that
     *       we do not waste time getting stuff the user does not need.
     */
    if( avformat_find_stream_info(pFormatCtx,NULL)<0){
        LOGE("NDK>>>%s","avformat_find_stream_info失败");
        return ;
    }
    LOGE("NDK>>>%s","成功");
    //  //输出视频信息
    //  LOGI("视频的文件格式:%s",pFormatCtx->iformat->name);
    //  LOGI("视频时长:%d", (pFormatCtx->duration)/1000000);

    //获取视频流的索引位置
    //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
    int v_stream_idx = -1;
    int i = 0;
    //遍历封装格式中所有流
    for (; i < pFormatCtx->nb_streams; ++i) {

        //获取视频流pFormatCtx->streams[i]
        //pFormatCtx->streams[i]->codec获取编码器
        //codec_type获取编码器类型
        //当前流等于视频 记录下标
        if (pFormatCtx->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO) {
            v_stream_idx = i;
            break;
        }
    }
    if (v_stream_idx==-1) {
        LOGE("没有找视频流")
    }else{
        LOGE("找到视频流")
    }

    //编码器上下文结构体,保存了视频(音频)编解码相关信息
    //得到视频流编码器
    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;

    //   每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。
    AVCodec *pCodec =avcodec_find_decoder(pCodecCtx->codec_id);

    //(迅雷看看,找不到解码器,临时下载一个解码器)
    if (pCodec == NULL)
    {
        LOGE("%s","找不到解码器\n");
        return;
    }else{
        LOGE("%s","找到解码器\n");
    }


    //打开解码器
    /**
     * 初始化 指定AVCodecContext去使用 给定的AVCodec
     * 在使用之前函数必须使用avcodec_alloc_context3()分配上下文。
     *
     * 以下函数 avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
     * avcodec_find_decoder() and avcodec_find_encoder() 提供了一个简便的得到一个解码器的方法
     *
     * @warning 这个函数线程不是安全的
     *
     * @note 在使用解码程序之前,始终调用此函数 (如 @ref avcodec_decode_video2()).
     * 下面是示例代码
     * @code
     * avcodec_register_all();
     * av_dict_set(&opts, "b", "2.5M", 0);
     * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
     * if (!codec)
     *     exit(1);
     *
     * context = avcodec_alloc_context3(codec);
     *
     * if (avcodec_open2(context, codec, opts) < 0)
     *     exit(1);
     * @endcode
     *
     *
     * @param avctx 要初始化的编码器
     * @param codec 用这个codec去打开给定的上下文编码器.如果 codec 不为空 那么必须
     * 事先用avcodec_alloc_context3和avcodec_get_context_defaults3传递给这个context,那么这个codec
     * 要么为NULL要么就是上面调用函数所使用的codec
     *
     * @param
     *
     * 选项填充AVCodecContext和编解码器私有选项的字典。返回时,此对象将填充未找到的选项。
     *
     * @return 返回0表示成功, 负数失败
     * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
     *      av_dict_set(), av_opt_find().
     */
    if(avcodec_open2(pCodecCtx,pCodec,NULL)==0){
        LOGE("%s","打开编码器成功\n");
    }else{
        LOGE("%s","打开编码器失败\n");
        return;
    }
    //输出视频信息
    LOGE("视频的文件格式:%s",pFormatCtx->iformat->name);
    //得到视频播放时长
    if(pFormatCtx->duration != AV_NOPTS_VALUE){
        int hours, mins, secs, us;
        int64_t duration = pFormatCtx->duration + 5000;
        secs = duration / AV_TIME_BASE;
        us = duration % AV_TIME_BASE;
        mins = secs / 60;
        secs %= 60;
        hours = mins/ 60;
        mins %= 60;
        LOGE("%02d:%02d:%02d.%02d\n", hours, mins, secs, (100 * us) / AV_TIME_BASE);

    }
    LOGE("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
    LOGE("解码器的名称:%s",pCodec->name);


    //存储一帧压缩编码数据。
    AVPacket *packet =av_malloc(sizeof(AVPacket));

    //输出转码文件地址
    FILE *fp_yuv = fopen(output_cstr,"wb+");

    //AVFrame用于存储解码后的像素数据(YUV)
    //内存分配
    AVFrame *pFrame = av_frame_alloc();

    //YUV420转码用
    AVFrame *pFrameYUV = av_frame_alloc();

    //avpicture_get_size()函数介绍:
    //
    /**
     * 如果给定存储图片的格式,那么计算给定的宽高所占用的大小
     *
     * @param pix_fmt   图片像素格式
     * @param width     图片宽
     * @param height     图片高
     * @return 返回计算的图片缓存大小或者错误情况下的负数错误代码
     *
     *
     * 这里计算缓存区的大小,但是没有分配,这里是用来后面转码使用
     */
    uint8_t *out_buffer = av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P,pCodecCtx->width,pCodecCtx->height));

    //初始化缓冲区
    /**
     * 基于指定的图片参数和提供的图片缓存区去设置图片字段
     *
     * 使用ptr所指向的图片数据缓存  填充图片属性
     *
     * 如果 ptr是空,这个函数仅填充图片行大小(linesize)的数组并且返回图片缓存请求的大小
     *
     * 要分配图片缓存并且再一次填充图片数据请使用 avpicture_alloc().
     * @param picture       要填充的图片
     * @param ptr           存储图片的数据的缓存区, or NULL
     * @param pix_fmt       图片像素格式
     * @param width         图片宽
     * @param height        图片高
     * @return 返回请求的字节大小,在错误的情况下返回负数
     *
     * @see av_image_fill_arrays()
     */
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等
    /**
     *分配和返回 SwsContext. You need it to perform
     * scaling/conversion operations using sws_scale().
     *
     * @param srcW 原始图宽
     * @param srcH 原始图高
     * @param srcFormat 原始图格式
     * @param dstW 目标图宽
     * @param dstH 不解释
     * @param dstFormat 不解释
     * @param flags 指定一个标志用于重新调整算法和选项
     *  具体参考:http://blog.csdn.net/leixiaohua1020/article/details/12029505
     * @return 一个指定分配内容的指针, 错误情况返回空
     * @note this function is to be removed after a saner alternative is
     *       written
     */
    struct SwsContext *sws_ctx =sws_getContext(pCodecCtx->width, pCodecCtx->height,pCodecCtx->pix_fmt,
                                               pCodecCtx->width, pCodecCtx->height,AV_PIX_FMT_YUV420P,
                                               SWS_BICUBIC, NULL, NULL, NULL);


    //标志位
    int got_picture, ret;

    //读取每一帧
    /**
     *返回下一帧的流
     * 此函数返回存储在文件中的内容,并且不会验证解码器有什么有效帧。
     * 函数将存储在文件中的帧进行分割 并且返回给每一个调用者。
     *
     * 函数不会删除在有效帧之间的无效数据 以便在可能解码过程中提供解码器最大的信息帮助
     * 如果 pkt->buf 是空的,那么这个对应数据包是有效的直到下一次调用av_read_frame()
     * 或者直到使用avformat_close_input().否则包无期限有效
     * 在这两种情况下 这个数据包当你不在需要的时候,你必须使用使用av_free_packet释放它
     * 对于视屏,数据包刚好只包含一帧.对于音频,如果它每一帧是一个已知固定大小的,那么他包含整数帧(如. PCM or ADPCM data)
     * 如果音频帧具有可变大小(如. MPEG audio),那么他只包含一帧
     * pkt->pts, pkt->dts and pkt->duration 始终在AVStream.time_base 单位设置正确的数值
     *(如果这个格式无法提供.那么进行猜测)
     * 如果视频格式有B帧那么pkt->pts可以是 AV_NOPTS_VALUE.如果你没有解压他的有效部分那么最好依靠pkt->dts
     *
     * @return 0表示成功, < 0 错误或者文结束
     */
    while(av_read_frame(pFormatCtx,packet)>=0){

        //一个包里有很多种类型如音频视频等 所以判断 这个包对应的流的在封装格式的下表
        //如果这个包是视频频包那么得到压缩的视频包
        if (packet->stream_index==v_stream_idx) {
            LOGE("测试");
            /**
             * 解码视频帧 从avpkt->data读取数据并且解码avpkt->size的大小后转化为图片.
             * 一些解码器可以支持在一个ACpacket中存在多帧的情况,像这样的解码器将只解码第一帧
             *
             * @warning  输入缓存区必须 实际读取的字节流小于 FF_INPUT_BUFFER_PADDING_SIZE,
             * 一些优化过的比特流 读取32位或者64字节 的时候可以一次性读取完
             *
             * @warning 在缓冲器的buf结尾设置0以确保被破坏的MPEG流不会发生超线程
             *
             * @note 有 CODEC_CAP_DELAY 才能设置一个在输入和输出之间的延迟,这些需要使用avpkt->data=NULL,
             *  在结束返回剩余帧数的时候avpkt->size=0
             *
             * @note  这个AVCodecContext 在数据包传入解码器之前必须调用avcodec_open2
             *
             *
             * @param avctx 解码器上下文
             *
             * @param[out] 解码的视频帧图片将会被存储在AVFrame.
             *                 使用av_frame_alloc 得到一个AVFrame,
             *                 编码器将会分配 使用  AVCodecContext.get_buffer2() 回调
             *                 的实际图片的内存.
             *                 当AVCodecContext.refcounted_frames 设置为1,这帧(frame)是引用计数,并且返回
             *                 的引用计数是属于调用者的.
             *             frame在长实际不使用的时候调用者必须调用av_frame_unref()就行释放
             *             如果av_frame_is_writable()返回1那么调用者可以安全的写入到这个frame中。
             *                 当AVCodecContext.refcounted_frames设置为0,返回的引用属于解码器,
             *                只有下次使用这个函数或者关闭或者刷新这个编码器之前有效。调用者不会写入它
             *
             *@param[in,out] got_picture_ptr 如果为0表示不能解压, 否者它不是0.
             *
             * @param[in] avpkt 这个输入的avpkt包含输入缓存区
             *              你能使用av_init_packet()创建像这样的packet然后设置数据和大小,
             *              一些解码器还可以添加一些其他字段 比如  flags&AV_PKT_FLAG_KEY  flags&AV_PKT_FLAG_KEY
             *          所有解码器都设计为尽可能少地使用
             *
             * @return 再错误时返回一个负数 , 否则返回使用字节数或者或者0(没有帧被解压返回0)otherwise the number of bytes
             *
             */
            ret=avcodec_decode_video2(pCodecCtx,pFrame,&got_picture,packet);
            if(ret>=0){
                LOGE("解压成功");
                //AVFrame转为像素格式YUV420,宽高
                //2 6输入、输出数据
                //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
                //4 输入数据第一列要转码的位置 从0开始
                //5 输入画面的高度
                sws_scale(sws_ctx,pFrame->data,pFrame->linesize,0,pCodecCtx->height,pFrameYUV->data,pFrameYUV->linesize);

                //输出到YUV文件
                //AVFrame像素帧写入文件
                //data解码后的图像像素数据(音频采样数据)
                //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感
                //U V 个数是Y的1/4
                int y_size = pCodecCtx->width * pCodecCtx->height;

                fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);

            }
        }
        av_free_packet(packet);
    }


    (*env)->ReleaseStringUTFChars(env,input,input_char);

    (*env)->ReleaseStringUTFChars(env,out,output_cstr);
    //关闭文件
    fclose(fp_yuv);

    //关闭资源
    av_frame_free(&pFrame);
    av_frame_free(&pFrameYUV);
    //关闭编码器上下文
    avcodec_close(pCodecCtx);
    //关闭输入流
    avformat_close_input(&pFormatCtx);
    //关闭封装格式
    avformat_free_context(pFormatCtx);
}

  • 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
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368

Studio版本DEMO

Android压缩图片和libjpeg库

前言:

在android开发时我们往往有如对图片如下的需求:
1. 希望压缩图片方便网络传输
2. 修改图片分辨率 防止在Android加载Bitmap的时候oom(内存溢出)

所以我就写了一个工具类Fjpeg封装二次采样和libjpeg进行图片压缩操作。
本文就是讲述我们写Fjpeg的学习过程

Fjpeg使用

源码地址

Fjpeg

封装libjpeg对图片进行压缩,和二次采样等代码封装

  1. 我们知道在Android 6.0以下 系统对图片的编码采用’定长编码’之后才采用‘哈夫曼编码’。
    所以本框架封装libjpeg 进行对图片进行编码 ,方便网络传输等
  2. 我们知道Android加载Bitmap图片时候 如果图片分辨率过大 我们需要对图片进行分辨率裁剪 在进行显示
    所以本框架封装了二次采样代码

注意:

libjpeg压缩不能改变图片分辨率,只能改变存储在硬盘中的大小。也就是说他的目的在于网络传输。
所以如果你想加载大图请使用本框架封装的二次采样代码即可

如何使用?

在你moudle的build.gradle中的dependencies添加以下代码

compile ‘com.fmy:fjpeg:1.0.0@aar

比如:

dependencies {
    ...   
    compile 'com.fmy:fjpeg:1.0.0@aar'
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

如何压缩图片(只改变在硬盘的存储大小)

        /**
         *  第一个参数:压缩后 文件输出路径,
         *  第二个参数:需要压缩Bitmap对象
         *  第三个参数:压缩质量 1-100  1是最小的
         *  第四个参数:是否启用哈夫曼编码
         *  第五个参数:回调 包含 开始前、错误、结束
         *  第六个参数:是否使用子线程
         */
        ImageUtils.compressQC(new File(Environment.getExternalStorageDirectory(), "测试剥离框架.jpg").getAbsolutePath(), bitmap, 1, true, new NativeCallBack() {
          //开始前回调
            @Override
            public void startCompress() {
                Log.d(TAG, "startCompress() called");
            }

            //错误回调
            @Override
            public void error(int errorNum, String description) {
                Log.d(TAG, "error() called with: errorNum = [" + errorNum + "], description = [" + description + "]");
            }

            //完成结束回调  如果发生错误讲不会回调次方法
            @Override
            public void finish(String filePath) {
                Log.d(TAG, "finish() called with: filePath = [" + filePath + "]");
            }
        }, 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

如何改变图片分辨率(让其Bitmap对象可以加载到内存中)

        //从mipmap中读取。此方法ImageUtils.compressPxSampleSize存在多个重载版本
        //如:从文件 、从资源、从io流、字节数组等
        Bitmap bitmap = ImageUtils.compressPxSampleSize(getResources(), R.mipmap.timg, 1000, 1000);
  • 1
  • 2
  • 3

关于重载版本

这里写图片描述

开始学习之旅:

Android的Bitmap对象在加载时 内存大小为:

宽的像素*高的像素*位图格式(如ARGB8888)
  • 1

注意:
这里的宽高是不是你放入文件夹中数据。需要换算
举例:
环境如下:
1. 一张图片宽高为:1000*1000
2. 格式为:ARGB8888(每一个像素点4个字节)
3. 将此图片放入:mipmap-xhdpi(320dpi density:2.0)
4. 设备density:4.0 densityDpi:640
(google规定160dpi density:1 那么 320dpi 的density自然等于2。关于这块基础知识不做详细介绍)
第一步:换算xhdpi和设备dpi
缩放比=设备密度/图文所在文件密度= 4/2 = 2

所以图片大小应为:

图片高 x 图片宽 x 缩放比 x 图片格式内存(图片每一个像素点颜色需要多少个字节保存)

最终: 4/2*1000*1000*8 = 16000000

口说无凭证那么请看如下单元测试:

这里写图片描述

输出结果:
这里写图片描述

补充知识的结论

所以可以知道一个Bitmap只会和它的 分辨率和图片格式有关。
所以我们的需求:
2. 修改图片分辨率 防止在Android加载Bitmap的时候oom(内存溢出) 只能修改分辨率和格式实现,我们一般从改变的图片分辨率入手,而libjpeg改变不是改变Bitmap内存大小 而是存储大小。

这里举例说明如下:
假设我有一张图片 在硬盘存储的时候大小为:100kb 然后经过libjpeg变成10kb 。但是分辨率没有改变。所以载入内存的时候大小是一样的。但是可以方便我们存储和传输。

修改图片分辨率 防止在Android加载Bitmap的时候oom(内存溢出)

我们来看一下我们如何解决这个需求

解决方案1:

利用canvas修改分辨率:

    /**
     *
     * 2. 尺寸压缩
     通过缩放图片像素来减少图片占用内存大小
     * @param bmp
     * @param file
     */

    public static void compressBitmapToFile(Bitmap bmp, File file){
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
        canvas.drawBitmap(bmp, null, rect, null);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        result.compress(Bitmap.CompressFormat.JPEG, 100 ,baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } 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

解决方案2:

我们最常见的修改采样率:
代码来自

     /** 
     * @param filePath   要加载的图片路径 
     * @param destWidth  显示图片的控件宽度 
     * @param destHeight 显示图片的控件的高度 
     * @return 
     */  
    public static Bitmap getBitmap(String filePath, int destWidth, int destHeight) {  
        //第一次采样  
        BitmapFactory.Options options = new BitmapFactory.Options();  
        //该属性设置为true只会加载图片的边框进来,并不会加载图片具体的像素点  
        options.inJustDecodeBounds = true;  
        //第一次加载图片,这时只会加载图片的边框进来,并不会加载图片中的像素点  
        BitmapFactory.decodeFile(filePath, options);  
        //获得原图的宽和高  
        int outWidth = options.outWidth;  
        int outHeight = options.outHeight;  
        //定义缩放比例  
        int sampleSize = 1;  
        while (outHeight / sampleSize > destHeight || outWidth / sampleSize > destWidth) {  
            //如果宽高的任意一方的缩放比例没有达到要求,都继续增大缩放比例  
            //sampleSize应该为2的n次幂,如果给sampleSize设置的数字不是2的n次幂,那么系统会就近取值  
            sampleSize *= 2;  
        }  
        /********************************************************************************************/  
        //至此,第一次采样已经结束,我们已经成功的计算出了sampleSize的大小  
        /********************************************************************************************/  
        //二次采样开始  
        //二次采样时我需要将图片加载出来显示,不能只加载图片的框架,因此inJustDecodeBounds属性要设置为false  
        options.inJustDecodeBounds = false;  
        //设置缩放比例  
        options.inSampleSize = sampleSize;  
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;  
        //加载图片并返回  
        return BitmapFactory.decodeFile(filePath, options);  
    }  
  • 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

采样率inSampleSize 必须是2的倍数.每次采样宽和高分辨率分别除以采样率。所以图片总大小减少采样率平方数。本文主讲libjpeg方面所以跳过详细说明

希望压缩图片方便网络传输

第一种方案利用Bitmap.compress()方法压缩。

 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.timg);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //第一个参数:压缩的格式
        //第二个参数:压缩自量1-100 1自量最小
        //第三个参数: 输出的字节
        bitmap.compress(Bitmap.CompressFormat.JPEG, 1, baos);
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(new File(Environment.getExternalStorageDirectory(), "测试画布来改变.jpg"));
            fileOutputStream.write(baos.toByteArray());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    baos.close();
                } catch (IOException 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

第二种利用libjpeg压缩。

//第一个参数:文件输出路径
//第二个参数:Bitmap对象
//第三个参数:压缩质量 1-100 1最小 
//第四个参数:是否启用哈夫曼变长编码(使用后更小)
NativeCompress.nativeLibJpegCompress(file.getAbsolutePath(), bitmap, 1, true);
  • 1
  • 2
  • 3
  • 4
  • 5

上面的代码是我们这篇文章要写的代码。我们先不用关心内部实现。只需要知道调用这个可以压缩图片

在Android5.0测试两个 图片压缩

这里写图片描述
我们发现压缩率使用libjpeg是远大于bitmap.compress方法的

在Android6.0测试两个 图片压缩

这里写图片描述
这时候我们发现两个压缩率是一模一样的。

解释Android5.0和6.0为什么压缩率差别

在Android和IOS中都采用skia图片库进行压缩和解码。
skia在05年被Google收购。
你可以在Android源码中看到\external\目录看到。当然libjpeg源码也在此目录。
在早期Android系统中 由于硬件不像今天晓龙835那样强大,内存多少G之类的。在执行某些图片压缩算法会很吃力,所以当时早期图片压缩的时候采用了 定长编码。而在6.0之后采用了哈夫曼编码

定长编码

假设你图片存储的文本内容为:
abbbbbbcde
我们知道在计算机中存储后:都只是一堆二进制文件而已
那么我们假设用如下方法存储:
用以下二进制代表 上面出现字母

字母 二进制
a 000
b 001
c 010
d 011
e 100

那么上述的文本对应存储的二进制为:
000 001 001 001 001 001 001 010 011 100

然后我们解析文件的时候根据上面的规则就可以翻译出存储后的内容

那么有没有比这个更好的规则呢?
答案是有的 哈夫曼编码

哈夫曼编码

哈夫曼树又称最优二叉树,那么我们一起看看上面例子利用二叉树编码后怎么样。
我们先根据上文的题目:有如下文本‘abbbbbbcde’

构造哈夫曼树。(关于怎么构造哈夫曼树这里不做讲述)
根据出现的次数 字母有如下权重:
a = 1
b = 6
c = 1
d = 1
e =1
所以可以得:
这里写图片描述

根据上面的编码:

b = 1
a = 000
c = 001
d = 011
e = 010

根据:abbbbbbcde
000 1 1 1 1 1 1 001 011 010

很明显哈夫曼编码在长度上有极大的优势。那么我们开始利用libjpeg完成吧

编译libjpeg前言

libjpeg为c语言编写 所以为了使用libjpeg我们需要把libjpeg编译成'静态库'或者‘动态库’。这里打架可以把 所谓的静态库和动态库当成java中jar包。
这里我们选择在linux下进行交叉编译。

关于交叉编译:我们知道c语言是无法跨平台使用的,我们Android虽然也是linux系统 但是在系统架构上 和我们用来编译的linux 多少是有不同的地方。所以我们要在编译的的时候指定交叉编译两个属性:

  • 1 一个目标系统文件库(可以理解为classPath路径,用c方面的说法就是某个操作系统/usr/目录下面的头文件和实现库):

    一般这个属性名字为->sysroot
    这个目录在NDK文件目录下platforms\android-xx\xxxx
    如:E:\Android\android-ndk-r9d\platforms\android-12\arch-arm\

    举个简单java例子:假设你写了一个Utils工具类给大家使用,类中有这样一个方法用于排序对象数组:传入参数为一个比较器。而工具类不知道某个类的对象谁大谁小,我需要调用则告诉我规则。在这里我们可以类推libjpeg为我们写Utils工具,而比较器的实现就像我们的操作系统,怎么比较是操作系统的事情,你只需要告诉我比较器在哪

  • 2 目标系统的编译软件(可以当成javac那样理解)

编译libjpeg步骤

环境说明:
– linux操作系统
– 已经下载好NDK文件到系统上 ,我用的是ndk-r14

  • 下载libjpeg源码到linux系统上
    下载地址
  • 下载后的源码是压缩包 所以解压
    这里写图片描述

  • 编写shell脚本 调用configure文件
    configure是一个libjpeg库内置的文件,只需要要简单的传入几个参数既可以生产Makefile
    那么现在编写一个编译出arm平台的共享库吧

#文件名:config.sh 放置到libjpeg解压后的目录。本例在jpeg-6b文件夹内 
NDK=/home/fmy/android-ndk-r14b
PLATFORM=$NDK/platforms/android-15/arch-arm
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
CC=$PREBUILT/bin/arm-linux-androideabi-gcc
HOST=arm
./configure --enable-shared --prefix=/home/fmy/libjpeg/jpeg-6b/fmy --exec-prefix=/home/fmy/libjpeg/jpeg-6b/fmy --host=$HOST CC="$CC --sysroot=$PLATFORM"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果你要使用的话只需要修改上面NDK变量的值为你系统NDK文件目录即可。
–prefix属性为你生成动态库目录
-exec-prefix属性为生成头文件目录

注意:关于–enable-shared 这个参数是用与编译生成动态库。但是你需要libtool 如果你不使用的话只能修改Makefile文件 生成静态库

  • 启动shell脚本,目录生产Makefile文件
    打开libjpeg-6b文件夹.然后使用如下命令
sh config.sh
  • 1
  • 根据情况修改Makefile文件(大多数不需要更改如ffmpeg x264库)
    上面的命令输入完成后 会生产Makefile文件 ,这个文件是用于编译和管理c工程的。因为linux不像windos那样有强的vs这样的工具(当成eclipse就好)。
    因为默认生产Makefile使用libtool编译的c的,而这里我们不使用这个工具。所以我们要进行修改.
vim Makefile
  • 1

上面的命令会打开linux文本编辑工具,然后按下i键,切换到输入模式修改两处处地方
这里写图片描述

  • 第一处位于39行
    原本为:
LIBTOOL = ./libtool
  • 1

修改为

LIBTOOL = 
  • 1

注意:=号后面需要保留一个空格。也就说别删除等号后面的空格 正确为 LIBTOOL=空格。后面两处修改也需要保留等号后面的空格

  • 第二处位于42和43行:
O = lo
A = la
  • 1
  • 2

修改后

O = o
A = a
  • 1
  • 2

目的很简单这样修改是为了不使用默认的libtool进行编译

  • 调用make 和make install 编译生产 共享库
make
make install
make distclean
  • 1
  • 2
  • 3

之后在你写的输出目录会得到静态库和头文件

创建Demo项目

  • 打开as创建项目
    记得勾选include C++ support。如果你没有这个选项请更新as到2.2以上版本
    这里写图片描述

  • 将编译好的静态库文件放入libs目录(不需要放入头文件)
    这里写图片描述

  • 修改moudle的build.gradle
    这里写图片描述

  • 放入头文件到cpp文件夹的include文件中这里写图片描述

  • 配置CMakeLists文件

#设置cmake最小支持版本
cmake_minimum_required(VERSION 3.4.1)

#你要编译c/c++源码
add_library( # 设置生成库文件名称
             native-lib
             # 编译生成共享库 当然你可以指定静态库===>>STATIC
             SHARED
             # 源文件地址
             src/main/cpp/native-lib.cpp
                       )

#设置distribution_DIR变量
#${CMAKE_SOURCE_DIR}为调用系统预先设定好的变量或者方法,这里返回CMakeLists文件地址
set(distribution_DIR   ${CMAKE_SOURCE_DIR}/libs/)


#添加Android的已经有的库文件 这里导入Log是用来在c代码层打印Log
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )
#同上
find_library( # Sets the name of the path variable.
              android-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              android )




#添加我们编译好的静态库到工程
#libjpeg.a
add_library(libjpeg-lib STATIC IMPORTED)
set_target_properties(libjpeg-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libjpeg.a)

#添加头文件地址
include_directories(src/main/cpp/include/)
#连接库文件
target_link_libraries(
                       native-lib
                       #这里必须加上 如果你是eclipse方式的Android.mk就不需要添加这个
                       #这里的作用保证AndroidBitmap_getInfo方法可用。你也可以通过find_library方法再添加
                       jnigraphics

                       ${log-lib}
                       ${android-lib}
                      #libjpegbither-lib
                      libjpeg-lib
                        )
  • 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

撸代JAVA层代码

我们在用libjpeg进行图片编码(压缩)的时候希望有回调。所以定义以下接口。

package org.jpegutil.fjpeg.minterface;
/**
 * Created by FMY on 2017/8/26.
 */

public interface NativeCallBack {

    void startCompress();

    void error(int errorNum, String description);

    void finish(String filePath);

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

这个时候我们还需要一个类java和c代码层交互。

package org.jpegutil.fjpeg;

import android.graphics.Bitmap;

import org.jpegutil.fjpeg.minterface.NativeCallBack;


/**
 * Created by FMY on 2017/8/25.
 */

public class NativeCompress {

    static {
        //加载我们编译好的动态库 他的全程libnative-lib.so大家在编译完成后再build/intermediates文件夹可以看到
        System.loadLibrary("native-lib");
    }

    /**
     * 采用libjpeg压缩图片 这个方法调用c代码
     *
     * @param outpath          用哈夫曼压缩后文件保存路径
     * @param bitmap           需要压缩的bitmap图片
     * @param CompressionRatio 质量1-100 1表示最低质量
     * @param isUseHoffman     是否使用哈夫曼编码
     */
    public static native void nativeLibJpegCompress(String outpath, Bitmap bitmap, int CompressionRatio, boolean isUseHoffman, NativeCallBack nativeCallBack);

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

到这里所有java层代码都写完了,接下来看我们编写c++代码

撸C++代码

我们创建工程的时候,会自动生成一个c++文件。
这个文件位于cpp/native-lib.cpp。
创建一个和java方法交互的c函数。那么问题来了怎么创建呢?
我们之前写了一个NativeCompress 类,里面nativeLibJpegCompress方法不知道各位还记得吗?我们说这个方法是用来和c交互的。。。

  • 你只需要把鼠标放在这个方法名上。
  • 按下ALT+ENTER(回车)
  • 在弹出的菜单中选择‘creat function XXXXXX’即可在native-lib.cpp中生成一个c函数。
    这里写图片描述

这时候我们再来看看native-lib.cpp生成了什么。
这里写图片描述

在这个函数上面写extern “C”(如果不写 c++会在编译的时候改变这个函数名,导致无法被java调用)
这里写图片描述

第一步.保存回调信息和输出到磁盘路等为全局引用

//定义一个别名 方便存储二进制数据
typedef typedef unsigned char BYTE;
//全局引用
jobject callBack;
JNIEnv *menv;
//防止c++的命名规范导致jni找不到方法
JNIEXPORT void JNICALL
Java_org_jpegutil_fjpeg_NativeCompress_nativeLibJpegCompress(JNIEnv *env, jobject instance,jstring outpath_, jobject bitmap,jint CompressionRatio,jboolean isUseHoffman,jobject nativeCallBack) {
    //文件输出地址
    const char *outpath = env->GetStringUTFChars(outpath_, 0);
    //用于保存bitmap的二进制数据
    BYTE *pixelscolor;

    //保存回调地址为全局引用
    callBack = env->NewGlobalRef(nativeCallBack);
    menv = env;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

校验传入Bitmap对象

我们这里只压缩像素格式为ARGB8888的,所以不是这个格式的时候我们应该回调错误信息。
那么我们怎么获得Bitmap的一些信息呢?
AndroidBitmapInfo对象是Bitmap的一些配置信息如宽高都可以获取到。
那么AndroidBitmapInfo又怎么获得呢?
我们可以调用AndroidBitmap_getInfo函数获取某个Bitmap对象的配置信息

// 得到bitmap一些信息
    AndroidBitmapInfo info;
    memset(&info, 0, sizeof(info));
    AndroidBitmap_getInfo(env, bitmap, &info);
    int w = info.width;
    int h = info.height;

    jclass nativeCallBackClass = env->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");

    //校验图片合法性
    if (w <= 0 || h <= 0) {
//        LOGE("发生错误:传入的图片宽度或者高度不小于等于0 【width:%d】【height:%d】", w, h);
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "图高度或者宽为0,【高:%d】 【 宽:%d】", h, w);
        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_HEIGHT_WIDTH_ERROR,
                                env->NewStringUTF(description));
        }

        //关闭资源
        freeResource();
        return;
    }

    //校验图片格式
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
//        LOGE("发生错误:传入的图片不合法");
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");

        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_FOMAT_ERROR,
                                env->NewStringUTF("图片格式错误"));
        }

        //关闭资源
        freeResource();
        return;
    }
  • 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

获取Bitmap对象像素信息

AndroidBitmap_lockPixels()方法可以锁定Bitmap对象,然后获取对应信息。记得获取完成后调用AndroidBitmap_unlockPixels()解锁对象

  //用于保存bitmap的二进制数据
    BYTE *pixelscolor;
//    LOGE("开始读取数据");
    //锁定bitmap 获取二进制数据
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
//
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE a, r, g, b;
    data = (BYTE *) malloc(w * h * 3);//每一个像素都有四个信息ARGB 并且ARGB8888每一个像素点为64位
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址

    int i, j;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //读取指针指向数据(这里指向bitmap二进制数据的指针)
            int color = *((int *) pixelscolor);

            //得到透明度
            //a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

            //保存data
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // **(data + 3) = *a;

            //地址偏移4个字节
            data += 3;
            pixelscolor += 4;
        }
    }
    //解锁bitmap
    AndroidBitmap_unlockPixels(env, bitmap);
  • 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

关于以下这一小段代码我相信很多人应该多少有疑惑

          //a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));
  • 1
  • 2
  • 3
  • 4

&:按位与。讲两个数字直接进行二进制比较,当且仅当两个二进制为1的时候结果为1 这一块
如:
2的二进制为 0010
3的二进制为 0011
那么结果
 0010
& 0011
———————-
 0010

>> x 带符号右移动:讲数字的二进制右移动x位 高位补符号为(正数0负数1)

如:2>>2 解释2向右移动两位
2的二进制为 0010 移动两位 0000
所以结果等于十进制的0

我们解析ARGB8888的图片,而这个格式需要占用4个字节。每个字节存储一个颜色。而4个字节正好不就是int的大小吗?
假设我们图片的第一个像素信息是:0xF0990011
那么透明度 A=F0
红色 R=99
绿色 G=00
蓝色 B=11

  • 那么我们举例说明其中之一如何取到透明度
    0xFF990011
    对象那么一长串的数字我们只需要FF那么我们进行&(按位或)运算 去掉990011这个冗余的数据
    所以 0xF0990011&0xFF000000=0xF0 00 00 00

然后右移24位 0xF0 00 00 00 = 0x 00 00 00 0F
结合来说: a = ((color & 0xFF000000) >> 24);

其他类比。。。。

我们再来回头看看整个方法:


//防止c++的命名规范导致jni找不到方法
JNIEXPORT void JNICALL
Java_org_jpegutil_fjpeg_NativeCompress_nativeLibJpegCompress(JNIEnv *env, jobject instance,
                                                             jstring outpath_, jobject bitmap,
                                                             jint CompressionRatio,
                                                             jboolean isUseHoffman,
                                                             jobject nativeCallBack) {
    //文件输出地址
    const char *outpath = env->GetStringUTFChars(outpath_, 0);


    //保存回调地址为全局引用
    callBack = env->NewGlobalRef(nativeCallBack);
    menv = env;

    // 得到bitmap一些信息
    AndroidBitmapInfo info;
    memset(&info, 0, sizeof(info));
    AndroidBitmap_getInfo(env, bitmap, &info);
    int w = info.width;
    int h = info.height;

    jclass nativeCallBackClass = env->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");

    //校验图片合法性
    if (w <= 0 || h <= 0) {
//        LOGE("发生错误:传入的图片宽度或者高度不小于等于0 【width:%d】【height:%d】", w, h);
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "图高度或者宽为0,【高:%d】 【 宽:%d】", h, w);
        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_HEIGHT_WIDTH_ERROR,
                                env->NewStringUTF(description));
        }

        //关闭资源
        freeResource();
        return;
    }

    //校验图片格式
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
//        LOGE("发生错误:传入的图片不合法");
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");

        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_FOMAT_ERROR,
                                env->NewStringUTF("图片格式错误"));
        }

        //关闭资源
        freeResource();
        return;
    }
    //用于保存bitmap的二进制数据
    BYTE *pixelscolor;
//    LOGE("开始读取数据");
    //锁定bitmap 获取二进制数据
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
//
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE a, r, g, b;
    data = (BYTE *) malloc(w * h * 3);//每一个像素都有四个信息ARGB 并且ARGB8888每一个像素点为64位
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址

    int i, j;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //读取指针指向数据(这里指向bitmap二进制数据的指针)
            int color = *((int *) pixelscolor);

            //得到透明度
            //*a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

            //保存data中
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // **(data + 3) = *a;

            //地址偏移4个字节
            data += 3;
            pixelscolor += 4;
        }
    }
    //解锁bitmap
    AndroidBitmap_unlockPixels(env, bitmap);
//    LOGE("读取数据完毕");
    //拷贝输出文件地址
    char *outPathBackup = (char *) malloc(sizeof(char) * (strlen(outpath) + 1));
    strcpy(outPathBackup, outpath);
//    LOGE("开始压缩");
    //压缩
    generateJPEG(tmpdata, w, h, CompressionRatio, outPathBackup, isUseHoffman);

    //释放资源
    env->ReleaseStringUTFChars(outpath_, outpath);
}

  • 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

我们看到最终调用generateJPEG ()函数 完成了全部步骤

贴上

int generateJPEG(BYTE *data, int w, int h, int quality,
                 const char *outfilename, jboolean optimize) {
    //回调java代码
    jclass nativeCallBackClass = menv->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");
    //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
    struct jpeg_compress_struct jcs;

    //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
    struct my_error_mgr jem;
    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
    //使用longjmp将跳转到这样
    if (setjmp(jem.setjmp_buffer)) {

        //关闭资源
        freeResource();
        return 0;
    }

    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE *f = fopen(outfilename, "wb");
    if (f == NULL) {
//        LOGE("打开文件失败");
        jmethodID errorMthodId = menv->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "以二进制打开读写文件路径[%s]失败",outfilename);
        if (callBack!=NULL) {

            menv->CallVoidMethod(callBack, errorMthodId,FILE_ERROR,
                                 menv->NewStringUTF(description));
        }
        //关闭资源
        freeResource();
        return 0;
    }

    //设置结构体的文件路径
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;//设置宽高
    jcs.image_height = h;
//  if (optimize) {
//      LOGI("optimize==ture");
//  } else {
//      LOGI("optimize==false");
//  }

    //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding, FALSE=Huffman */
    jcs.arith_code = false;
    int nComponent = 3;
    /* 颜色的组成 rgb,三个 # of color components in input image */
    jcs.input_components = nComponent;
    //设置结构体的颜色空间为rgb
    jcs.in_color_space = JCS_RGB;
//  if (nComponent == 1)
//      jcs.in_color_space = JCS_GRAYSCALE;
//  else
//      jcs.in_color_space = JCS_RGB;

    //全部设置默认参数/* Default parameter setup for compression */
    jpeg_set_defaults(&jcs);
    //是否采用哈弗曼表数据计算 品质相差5-10倍
    jcs.optimize_coding = optimize;
    //设置质量 quality是个0~100之间的整数,表示压缩比率
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩,(是否写入全部像素)
    jpeg_start_compress(&jcs, TRUE);

    JSAMPROW row_pointer[1];
    int row_stride;
    //一行的rgb数量
    row_stride = jcs.image_width * nComponent;
    //一行一行遍历
    while (jcs.next_scanline < jcs.image_height) {
        //得到一行的首地址
        row_pointer[0] = &data[jcs.next_scanline * row_stride];

        //此方法会将jcs.next_scanline加1
        jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
    }
    jpeg_finish_compress(&jcs);//结束
    jpeg_destroy_compress(&jcs);//销毁 回收内存
    fclose(f);//关闭文件



    jmethodID pID = menv->GetMethodID(nativeCallBackClass, "finish",
                                      "(Ljava/lang/String;)V");
    if (callBack!=NULL) {

        menv->CallVoidMethod(callBack,pID,menv->NewStringUTF(outfilename));
    }
    //关闭资源
    freeResource();
//    LOGE("完成");
    return 1;
}
  • 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

最终我们看下完整代码

#include <jni.h>
#include <string.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <stdio.h>
#include <android/bitmap.h>
#include <setjmp.h>
#include <math.h>
#include <stdint.h>
#include <time.h>
#include "mconst.h"
//告诉编译器以下文件是用c文件
extern "C" {
#include "jpeglib.h"
#include "jerror.h"     /* Common decls for cjpeg/djpeg applications */
#include "jmorecfg.h"   /* for version message */
#include "jconfig.h"
}
#define LOG_TAG "libjpeg"
#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
//全局引用
jobject callBack;
JNIEnv *menv;

//关闭资源调用此方法
void freeResource() {

    menv->DeleteGlobalRef(callBack);
    callBack = NULL;
    menv = NULL;
}

//定义一个别名 方便存储二进制数据
typedef typedef unsigned char BYTE;

#define true 1
#define false 0

char *error;
struct my_error_mgr {

    struct jpeg_error_mgr pub;
    jmp_buf setjmp_buffer;
};

typedef struct my_error_mgr *my_error_ptr;



//错误的方法回调
METHODDEF(void) my_error_exit(j_common_ptr cinfo) {
    my_error_ptr myerr = (my_error_ptr) cinfo->err;
    (*cinfo->err->output_message)(cinfo);
    error = (char *) myerr->pub.jpeg_message_table[myerr->pub.msg_code];
//    LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,
//         myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
    // LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
//  LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
//  LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
    jclass nativeCallBackClass = menv->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");
    jmethodID errorMthodId = menv->GetMethodID(nativeCallBackClass, "error",
                                              "(ILjava/lang/String;)V");
    char description[100];
    sprintf(description, "jpeg_message_table[%d]:%s", myerr->pub.msg_code,
            myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
    if (callBack!=NULL) {

        menv->CallVoidMethod(callBack, errorMthodId, INTERNAL_ERROR,
                             menv->NewStringUTF(description));
    }


    //跳转setjmp 并且返回值为1结束
    longjmp(myerr->setjmp_buffer, 1);



}

int generateJPEG(BYTE *data, int w, int h, int quality,
                 const char *outfilename, jboolean optimize) {
    //回调java代码
    jclass nativeCallBackClass = menv->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");
    //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
    struct jpeg_compress_struct jcs;

    //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
    struct my_error_mgr jem;
    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
    //使用longjmp将跳转到这样
    if (setjmp(jem.setjmp_buffer)) {

        //关闭资源
        freeResource();
        return 0;
    }

    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE *f = fopen(outfilename, "wb");
    if (f == NULL) {
//        LOGE("打开文件失败");
        jmethodID errorMthodId = menv->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "以二进制打开读写文件路径[%s]失败",outfilename);
        if (callBack!=NULL) {

            menv->CallVoidMethod(callBack, errorMthodId,FILE_ERROR,
                                 menv->NewStringUTF(description));
        }
        //关闭资源
        freeResource();
        return 0;
    }

    //设置结构体的文件路径
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;//设置宽高
    jcs.image_height = h;
//  if (optimize) {
//      LOGI("optimize==ture");
//  } else {
//      LOGI("optimize==false");
//  }

    //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding, FALSE=Huffman */
    jcs.arith_code = false;
    int nComponent = 3;
    /* 颜色的组成 rgb,三个 # of color components in input image */
    jcs.input_components = nComponent;
    //设置结构体的颜色空间为rgb
    jcs.in_color_space = JCS_RGB;
//  if (nComponent == 1)
//      jcs.in_color_space = JCS_GRAYSCALE;
//  else
//      jcs.in_color_space = JCS_RGB;

    //全部设置默认参数/* Default parameter setup for compression */
    jpeg_set_defaults(&jcs);
    //是否采用哈弗曼表数据计算 品质相差5-10倍
    jcs.optimize_coding = optimize;
    //设置质量 quality是个0~100之间的整数,表示压缩比率
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩,(是否写入全部像素)
    jpeg_start_compress(&jcs, TRUE);

    JSAMPROW row_pointer[1];
    int row_stride;
    //一行的rgb数量
    row_stride = jcs.image_width * nComponent;
    //一行一行遍历
    while (jcs.next_scanline < jcs.image_height) {
        //得到一行的首地址
        row_pointer[0] = &data[jcs.next_scanline * row_stride];

        //此方法会将jcs.next_scanline加1
        jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
    }
    jpeg_finish_compress(&jcs);//结束
    jpeg_destroy_compress(&jcs);//销毁 回收内存
    fclose(f);//关闭文件



    jmethodID pID = menv->GetMethodID(nativeCallBackClass, "finish",
                                      "(Ljava/lang/String;)V");
    if (callBack!=NULL) {

        menv->CallVoidMethod(callBack,pID,menv->NewStringUTF(outfilename));
    }
    //关闭资源
    freeResource();
//    LOGE("完成");
    return 1;
}

extern "C"



//防止c++的命名规范导致jni找不到方法
JNIEXPORT void JNICALL
Java_org_jpegutil_fjpeg_NativeCompress_nativeLibJpegCompress(JNIEnv *env, jobject instance,
                                                             jstring outpath_, jobject bitmap,
                                                             jint CompressionRatio,
                                                             jboolean isUseHoffman,
                                                             jobject nativeCallBack) {
    //文件输出地址
    const char *outpath = env->GetStringUTFChars(outpath_, 0);


    //保存回调地址为全局引用
    callBack = env->NewGlobalRef(nativeCallBack);
    menv = env;

    // 得到bitmap一些信息
    AndroidBitmapInfo info;
    memset(&info, 0, sizeof(info));
    AndroidBitmap_getInfo(env, bitmap, &info);
    int w = info.width;
    int h = info.height;

    jclass nativeCallBackClass = env->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");

    //校验图片合法性
    if (w <= 0 || h <= 0) {
//        LOGE("发生错误:传入的图片宽度或者高度不小于等于0 【width:%d】【height:%d】", w, h);
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "图高度或者宽为0,【高:%d】 【 宽:%d】", h, w);
        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_HEIGHT_WIDTH_ERROR,
                                env->NewStringUTF(description));
        }

        //关闭资源
        freeResource();
        return;
    }

    //校验图片格式
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
//        LOGE("发生错误:传入的图片不合法");
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");

        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_FOMAT_ERROR,
                                env->NewStringUTF("图片格式错误"));
        }

        //关闭资源
        freeResource();
        return;
    }
    //用于保存bitmap的二进制数据
    BYTE *pixelscolor;
//    LOGE("开始读取数据");
    //锁定bitmap 获取二进制数据
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
//
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE a, r, g, b;
    data = (BYTE *) malloc(w * h * 3);//每一个像素都有四个信息ARGB 并且ARGB8888每一个像素点为64位
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址

    int i, j;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //读取指针指向数据(这里指向bitmap二进制数据的指针)
            int color = *((int *) pixelscolor);

            //得到透明度
            //*a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

            //保存data中
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // **(data + 3) = *a;

            //地址偏移4个字节
            data += 3;
            pixelscolor += 4;
        }
    }
    //解锁bitmap
    AndroidBitmap_unlockPixels(env, bitmap);
//    LOGE("读取数据完毕");
    //拷贝输出文件地址
    char *outPathBackup = (char *) malloc(sizeof(char) * (strlen(outpath) + 1));
    strcpy(outPathBackup, outpath);
//    LOGE("开始压缩");
    //压缩
    generateJPEG(tmpdata, w, h, CompressionRatio, outPathBackup, isUseHoffman);

    //释放资源
    env->ReleaseStringUTFChars(outpath_, outpath);
}

  • 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
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292

参考文献:

Android开发之高效加载Bitmap

编译好的静态库和头文件

编译好的文件下载地址

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函数确保每次调用都是你预期的。现在:祝玩得开心!

KMP next数组讲解

本文只讲解KMP子串keyString(下标用j表示),中和源字符串SoureceString(下标i表示)某次匹配失败后。下次匹配j的取值。
关于KMP算法入门可以看看其他博文(最主要网上关于这个算法写烂了,我这里只记录下我学习不懂的地方)

前言

在学KMP算法的时候一直感觉next数组的实现是让我最头痛的。所以现在回过头写下笔记,方便以后再看。

规则:
1. keyString(子串)的next数组大小为keyString的长度。
2. next第一个元素为-1
3. next数组下标数等于 子串前缀和后缀的匹配数

前缀概念:
子字符串的从第一个元素开始到第x元素 和 字符第y个元素到最后一个字符串相等.(x和y可以相等)

注意 后缀的概念:有个误区 是指子字符串第z个元素的时候 把z最为最后一个元素

eg:
字符串 keyString =“abcabcx”

这里写图片描述

我们可以把.下标为1的元素(即是第二个元素)作为后缀。那么前缀是a。后缀是b
我们把下标为5的元素作为后缀。那么前缀是abc(下标为0,1,2)后缀是abc(下标为 3,4,5)

上面对应next数组
这里写图片描述
当子串到下标为6的位置和源字符串匹配失败的时候,下次匹配直接到下标为3的位置

ok 现在我们开始现实某个字符串next数组的实现,首先写一个方法为get

    //java语言
    // T为子字符串
    // next数组 
    // 当方法结束的时候 next包含对应T的射影数值
    public static void get(String T, int[] next) {
    ....
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们先实现前两个规则

规则:
1. keyString(子串)的next数组大小为keyString的长度。
2.  next第一个元素为-1
  • 1
  • 2
  • 3

如下

//默认传入的next数组大小为子字符串大小 所以实现规则2即可
public static void get(String T, int[] next) {

        int i, j;

        j = -1;
        next[0] = -1;
        i = 0;

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

最后一个规则:

next数组下标数等于 子串前缀和后缀的匹配数.先看一下next完成的案例在说

算法核心也在这此处,再回头看看如下字符串和对应next数组
这里写图片描述

理解next数组的含义
我们看一下‘x’对应next为6
意味着:
1. 字符串从0到5作为前缀。(为什么不到6?因为数组下标从0开始)
2. 从x下标减6开始到x前面一个数作为后缀。
3. 有6个循环数。
这里写图片描述

先看最终代码(可能会疑惑在回溯部分,先暂时看看后面再解释):

 public static void get(String T, int[] next) {

        int i, j;

        j = -1;
        next[0] = -1;
        i = 0;
        while (i < T.length() - 1) {

            if (j == -1 || T.charAt(i) == T.charAt(j)) {

                i++;
                j++;
                next[i] = j;
            } else {
                j = next[j];//回溯
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

关于回溯我们举例说明说明

我们假设数组末尾追加一个字母d(下标为10)

这里写图片描述

假设我们的算法遍历到下标为10 数值为x的元素。
此时

j=6. i = 9
  • 1

此时算法 T.chatAt(i) 不等 T.chatAt(j)所以走else路径

 j = next[j];//回溯
  • 1
  • 将j回溯到next[6]的位置,如果此时相等那么 即T.chatAt(i)== T.chatAt(j)。那么就结束了。不然继续回溯。我当时在想为什么j会回溯到next[j]。原因如下,j代表着有多少个后缀和前缀数相等的数量。那么我们将设 下标为10(数值x)的元素等于下标为6(数值a)的元素 。循环到此的时候j=6代表着在下标为9的元素之前有6个相等数。(0,1,2,3,4,5 和 3,4,5,6,7,8)。那么6 和 9 相等就代表着相同后缀有6个。所以我们只需要判断字符串6和9即可(因为此算法如果前面的数字前缀相等那么会建立再次基础上 进行判断【j= 6 就是一个基础,它代表着 前缀的后面一个数 也代表着前缀和后缀相同数】)。

  • 那么假设不相等的情况的解释:
    继续假设运行到
    我们的算法遍历到下标为10 数值为x的元素。
    此时

j=6. i = 9
  • 1

(0,1,2,3,4,5 和 3,4,5,6,7,8前面判断已经相等),但是数组下标6和9不相等 进行j回溯到next[6]的位置,也就是 j = 3 的位置( next[6] = 3),
j代表某前缀的最后一个数的后面一个数。比如说 j =3 那么前缀就是 0 1 2。我们在我们 j =6 i =9 匹配失败后 你可以让 j = j-1继续匹配 T.chatAt(9),但是这是多余的。

假设 数组下标6 和9 匹配失败 进行 j=j-1
T.chatAt(6) != T.chatAt(9);
j = j-1 = 5;
那么
T.chatAt(5) == T.chatAt(9);
也就是if判断条件成立 可得
0 1 2 3 4 5 和 4 5 6 7 8 9相等.
那么可以得以下结论:
T.chatAt(5) == T.chatAt(9);
T.chatAt(5) == T.chatAt(8);
T.chatAt(8) == T.chatAt(9);
这三个不难推理出

继续
T.chatAt(4) == T.chatAt(8);
T.chatAt(9) == T.chatAt(4);
T.chatAt(9) == T.chatAt(5);

也就是 7==8==9 和 1==2==3

最终你将推出 0==1==2==3==4==5 ==7==8==9
显然这会和next数组原有数组数值发生冲突。顾不能直接j= j-1。当然如果数组前缀和后缀所有元素都相等 时候可以推出next[j] = j -1