Hongyang - 钱柜娱乐开户 http://static.blog.csdn.net/images/logo.gif 生命不息,奋斗不止,万事起于忽微,量变引起质变 /lmj623565791 zh-cn 5 2018/02/09 09:13:25 Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/79278864 /lmj623565791/article/details/79278864 lmj623565791 2018/02/07 12:54:12

本文已在我的公众号hongyang钱柜娱乐开户原创发布。
转载请标明出处:
本文出自:涨鸿洋的博客

前段时间在dota群,一哥们出去面试,回顾面试题的时候,说问到了枚举。

作为一名钱柜娱乐开户选手,谈到枚举,那肯定是:

钱柜娱乐开户上不应该使用枚举,占内存,应该使用@XXXDef注解来替代,balabala…

这么一回答,心里美滋滋。

没想到面试官问了句:

  • 枚举的原理是什么?你说它占内存到底占多少内存呢,如何佐证?

听到这就慌了,没了解过呀。

下面说第一个问题(没错还有第二个问题)。

枚举的本质

有篇文章:

/mhmyqn/article/details/48087247

写得挺好的。

下面还是要简述一下,我们先写个枚举类:

public enum Animal {
    DOG,CAT
}

看着这代码,完全看不出来原理。不过大家应该都知道java类编译后会产生class文件。

越接近底层,本质就越容易暴露出来了。

我们先javac搞到Animal.class,然后通过javap命令看哈:

javap Animal.class

输出:

public final class Animal extends java.lang.Enum<Animal> {
  public static final Animal DOG;
  public static final Animal CAT;
  public static Animal[] values();
  public static Animal valueOf(java.lang.String);
  static {};
}

其实到这里我们已经大致知道枚举的本质了,实际上我们编写的枚举类Animal是继承自Enum的,每个枚举对象都是static final的类对象。

还想知道更多的细节怎么办,比如我们的对象什么时候初始化的。

我们可以添加-c参数,对代码进行反编译。

你可以使用javap -help 查看所有参数的含义。

javap -c Animal.class

输出:

public final class Animal extends java.lang.Enum<Animal> {
  public static final Animal DOG;

  public static final Animal CAT;

  public static Animal[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[LAnimal;
       3: invokevirtual #2                  // Method "[LAnimal;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[LAnimal;"
       9: areturn

  public static Animal valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class Animal
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class Animal
       9: areturn

  static {};
    Code:
       0: new           #4                  // class Animal
       3: dup
       4: ldc           #7                  // String DOG
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field DOG:LAnimal;
      13: new           #4                  // class Animal
      16: dup
      17: ldc           #10                 // String CAT
      19: iconst_1
      20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #11                 // Field CAT:LAnimal;
      26: iconst_2
      27: anewarray     #4                  // class Animal
      30: dup
      31: iconst_0
      32: getstatic     #9                  // Field DOG:LAnimal;
      35: aastore
      36: dup
      37: iconst_1
      38: getstatic     #11                 // Field CAT:LAnimal;
      41: aastore
      42: putstatic     #1                  // Field $VALUES:[LAnimal;
      45: return
}

好了,现在可以分析代码了。

但是,这代码看起来也太头疼了,我们先看一点点:

static中部分代码:

0: new           #4                  // class Animal
3: dup
4: ldc           #7                  // String DOG
6: iconst_0
7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
10: putstatic     #9                  // Field DOG:LAnimal;

大致含义就是new Animal(String,int),然后给我们的静态常量DOG赋值。

好了,不看了,好烦。我们转念想一下,如果这个字节码咱们能看懂,那就是有规则的,只要有规则,肯定有类似翻译类的工具,直接转成java代码的。

确实有,比如jad:

http://www.javadecompilers.com/jad

我们先下载一份,很小:

meiju01.png

命令也很简单,执行:

./jad -sjava Animal.class

就会在当前目录生成java文件了。

输出如下:

public final class Animal extends Enum
{

    public static Animal[] values()
    {
        return (Animal[])$VALUES.clone();
    }

    public static Animal valueOf(String s)
    {
        return (Animal)Enum.valueOf(Animal, s);
    }

    private Animal(String s, int i)
    {
        super(s, i);
    }

    public static final Animal DOG;
    public static final Animal CAT;
    private static final Animal $VALUES[];

    static 
    {
        DOG = new Animal("DOG", 0);
        CAT = new Animal("CAT", 1);
        $VALUES = (new Animal[] {
            DOG, CAT
        });
    }
}

到这,我相信你知道我们编写的枚举类:

public enum Animal {
    DOG,CAT
}

最终生成是这样的类,那么对应的我们所使用的方法也就都明白了。此外,你如何拿这样的类,跟两个静态INT常量比内存,那肯定是多得多的。

其次,我们也能顺便回答,枚举对象为什么是单例了。

并且其Enum类中对readObject和clone方法都进行了实现,看一眼你就明白了。

本文并不是为了去讨论枚举的原理,而是想要给大家说明的是很多“语法糖”类似的东西,都能按照这样的思路去了解它的原理。

下面我们再看一个,听起来稍微高端一点的:

  • 动态代理

动态代理

这个比较出名的就是retrofit了。

问:retrofit的原理是?

答:基于动态代理,然后balabal...

问:那么动态代理的原理是?

答:...

我们依然从一个最简单的例子开始。

我们写一个接口:

public interface IUserService{
    void login(String username, String password);
}

然后,利用动态代理去生成一个代理对象,去调用login方法:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class Test{
    public static void main(String[] args){

        IUserService userService = (IUserService) Proxy.newProxyInstance(IUserService.class.getClassLoader(),
                new Class[]{IUserService.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        System.out.println("method = " + method.getName() +" , args = " + Arrays.toString(args));

                        return null;
                    }
                });

        System.out.println(userService.getClass());

        userService.login("zhy","123");
    }
}

好了,这应该是最简单的动态代理的例子了。

当我们去调研userService.login方法,你会发现InvocationHandler的invoke方法调用了,并且输出了相关信息。

怎么会这么神奇呢?

我们写了一个接口,就能产生一个该接口的对象,然后我们还能拦截它的方法。

继续看:

javac Test.java,得到class文件。

然后调用:

java Test

输出:

class com.sun.proxy.$Proxy0
method = login , args = [zhy, 123]

可以看到当我们调用login方法的时候,invoke中拦截到了我们的方法,参数等信息。

retrofit的原理其实就是这样,拦截到方法、参数,再根据我们在方法上的注解,去拼接为一个正常的Okhttp请求,然后执行。

想知道原理,根据我们枚举中的经验,肯定想看看这个

com.sun.proxy.$Proxy0 // userService对象输出的全路径

这个类的class文件如何获取呢?

很简单,你在main方法的第一行,添加:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");  

然后重新编译、执行,就会在当前目录看到了。

MacBook-Pro:tmp zhanghongyang01$ tree 
.
├── IUserService.class
├── IUserService.java
├── Test$1.class
├── Test.class
├── Test.java
└── com
    └── sun
        └── proxy
            └── $Proxy0.class

3 directories, 6 files

然后,还想通过javap -c来看么~~

这里写图片描述

还是拿出我们刚才下载的jad吧。

执行:

./jad -sjava com/sun/proxy/\$Proxy0.class 

在jad的同目录,你就发现了Proxy0的java文件了:

package com.sun.proxy;

import IUserService;
import java.lang.reflect.*;

public final class $Proxy0 extends Proxy
    implements IUserService
{

    public $Proxy0(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }

    public final void login(String s, String s1)
    {
        super.h.invoke(this, m3, new Object[] {
            s, s1
        }); 
    }


    private static Method m3;

    static 
    {
        m3 = Class.forName("IUserService").getMethod("login", new Class[] {
            Class.forName("java.lang.String"), Class.forName("java.lang.String")
        });

    }
}

为了便于理解,删除了一些equals,hashCode等方法。

你可以看到,实际上为我们生成一个实现了IUserSevice的类,我们调用其login方法,实际上就是调用了:

 super.h.invoke(this, m3, new Object[] {
            s, s1
        }); 

m3即为我们的login方法,静态块中初始化的。剩下是我们传入的参数。

那我们看super.h是什么:

package java.lang.reflect;
public class Proxy{
    protected InvocationHandler h;
}

就是我们自己创建的InvocationHandler对象。

看着这个类,再想login方法,为什么会回调到InvocationHandler的invoke方法,你还觉得奇怪么~~

好了,实际上这个哥们面试距离现在挺久了,终于抽空写完了,希望大家有一定的收获~

作者:lmj623565791 发表于 2018/02/07 12:54:12 原文链接 /lmj623565791/article/details/79278864
阅读:281 评论:2 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/79081656 /lmj623565791/article/details/79081656 lmj623565791 2018/01/17 09:48:14

本文已在我的公众号hongyang钱柜娱乐开户原创发布。
你可以在此查看历史文章合集
历史文章合集

概述

今天逛简书的时候,发现了一个库:

主要功能是这样的,先口述一下,当打开app,可以通过浏览器访问一个地址,然后通过浏览器可以给手机上上传apk(也支持已有apk删除),然后手机端可以安装、卸载该apk。

三张图就明白了:

应用启动后:

gekong02.png

然后PC端访问:

gekong01.gif

拖拽apk上传,即可上传到手机端。

gekong03.png

ok,大致介绍清楚了。

注意一定要在同一个网段。

先不谈其用处到底有多大,很多时候我看到一个项目的时候,很少考虑其能干嘛,考虑最多的是它是如何实现的,我会么,不会那就学,至于能干嘛,那要等我学会之后?

那么思考下他的实现,这种上传文件的方式,在PC端更加常见,上传文件到服务器。

说到这,就可以想到,可能这个app在手机端搭建了一个服务器。

恩,没错就是这样的,在手机端搭建了一个服务器,这样就可以通过html,将PC端的文件传给手机端,然后手机端收到后再同步界面。

同时,也可以将手机上Sdcard上的文件,完全在PC上呈现。

手机端的Server利用的是该库:

解析源码的事情就不做了,有兴趣可以自己学习下,接下来开始正片。

一个群友的问题

之所以会关注到这个库,是因为在wan钱柜娱乐开户群,有个哥们连续问了好久的一个问题,问题是:

  • 如何通过浏览器输入一个地址播放手机上的视频

当时也很多人回答,回答的核心都是正确的。

当然我恰好看到这个库,之前也没推送过相关内容,所以我决定写个简易的Demo.

当然是Demo就没有什么美观可言了,仅为快速实现效果。

效果图是这样的:

gekong04.gif

页面上显示手机上的视频列表,然后点击某个视频,即开始播放该视频。

有了上例参考,非常简单。

注:部分代码直接从上例copy。
该案例需要网络和Sdcard权限!

先把服务器搭起来

依赖库

首先,依赖下我们搭建Server需要用到的库:

compile 'com.koushikdutta.async:钱柜娱乐开户async:2.+'

编写简易html

然后我们在assets下编写一个html文件用于浏览器访问,index.html

最简单的即可:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>

<body>

嘿嘿嘿,连通了...

</body>

</html>

启动服务,监听端口


public class MainActivity extends AppCompatActivity {
    private AsyncHttpServer server = new AsyncHttpServer();
    private AsyncServer mAsyncServer = new AsyncServer();

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


        server.get("/", new HttpServerRequestCallback() {
            @Override
            public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
                try {
                    response.send(getIndexContent());
                } catch (IOException e) {
                    e.printStackTrace();
                    response.code(500).end();
                }
            }
        });

        server.listen(mAsyncServer, 54321);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (server != null) {
            server.stop();
        }
        if (mAsyncServer != null) {
            mAsyncServer.stop();
        }
    }

    private String getIndexContent() throws IOException {
        BufferedInputStream bInputStream = null;
        try {
            bInputStream = new BufferedInputStream(getAssets().open("index.html"));
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int len = 0;
            byte[] tmp = new byte[10240];
            while ((len = bInputStream.read(tmp)) > 0) {
                baos.write(tmp, 0, len);
            }
            return new String(baos.toByteArray(), "utf-8");
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (bInputStream != null) {
                try {
                    bInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

可以看到很简单,创建AsyncHttpServer对象,我们在onCreate中调用get,对外设置一个get型的url监听,监听的url是/即根目录。

然后调用listen,传入端口号54321,开启对该端口的监听。

onDestroy的时候停止服务器。

当捕获到对”/”的访问时,读取assets下的index.html返回给浏览器。

记得添加网络权限。

好了,运行demo,测试一下。

输入地址,你的手机的IP:端口号。

注意电脑和手机在同一个网段!

然后你应该看到如下效果图:

gekong05.png

如果没看到,那不用往下了,先找问题吧~

完善Demo

接下来,我们将手机上的mp4返回让其在浏览器上显示。

很简单,既然我们可以监听/,返回一个index.html,我们就能监听另一个url,返回文件目录。

server.get("/files", new HttpServerRequestCallback() {
    @Override
    public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
        JSONArray array = new JSONArray();
        File dir = new File(Environment.getExternalStorageDirectory().getPath());
        String[] fileNames = dir.list();
        if (fileNames != null) {
            for (String fileName : fileNames) {
                File file = new File(dir, fileName);
                if (file.exists() && file.isFile() && file.getName().endsWith(".mp4")) {
                    try {
                        JSONObject jsonObject = new JSONObject();
                        jsonObject.put("name", fileName);
                        jsonObject.put("path", file.getAbsolutePath());
                        array.put(jsonObject);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        response.send(array.toString());
    }
});

我们监听/files这个Url,然后返回Sdcard根目录的视频文件,拼接成JSON返回。

这里如果你重新启动,在浏览器上输入:

http://192.168.1.100:54321/files

会看到一堆JSON数据:

gekong06.png

但是我们需要在刚才的html上显示,所以这个请求应该是刚才的Html页面发起:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="jquery-1.7.2.min.js" type="text/javascript"></script>

    <title>文档的标题</title>
    <script type="text/javascript">
        $(function() {
            var now = new Date();
            var url = 'files' + '?' + now.getTime();
            // 请求JSON数据
            $.getJSON(url, function(data) {
                // 编辑JSON数组
                for (var i = 0; i < data.length; i++) {
                    // 为每个对象生成一个li标签,添加到页面的ul中
                    var $li = $('<li>' + data[i].name + '</li>');
                    $li.attr("path", data[i].path);
                    $("#filelist").append($li);

                }
            });
        });

    </script>
</head>

<body>
    <ul id="filelist" style="float:left;"></ul>

</body>

</html>

可能很多朋友没了解过js,不过应该能看明白,$.getJSON获取返回的JSON数组,然后遍历为每个Json对象生成一个li标签,添加到页面上。

这里用了jquery,对于js的也需要也请求处理,这里省略了,很简单,看源码即可。

此时访问,已经可以显示出视频目录了:

gekong07.png

接下来就是点击播放了,在html里面有个标签叫video用于播放视频的,他有个src属性用于设置播放的视频路径。

所以我们要做的仅为:

  • 点击名字,拿到该视频对应的url,然后设置给video的src属性即可。

那么视频的url是什么?

刚才我们返回了视频的路径,所以我们只要再监听一个url,将根据传入的视频路径,将视频文件流返回即可。

server.get("/files/.*", new HttpServerRequestCallback() {
    @Override
    public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
        String path = request.getPath().replace("/files/", "");
        try {
            path = URLDecoder.decode(path, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        File file = new File(path);
        if (file.exists() && file.isFile()) {
            try {
                FileInputStream fis = new FileInputStream(file);
                response.sendStream(fis, fis.available());
            } catch (Exception e) {
                e.printStackTrace();
            } 
            return;
        }
        response.code(404).send("Not found!");
    }
});

我们又监听了一个url为files/xxx.*,捕获到之后,拿到文件名,去SDCard找到该文件,返回文件流即可。

html端的代码为:

<script type="text/javascript">
    $(function() {
        var now = new Date();
        // 拿到video对象
        var $video = $("#videoplayer");
        var url = 'files' + '?' + now.getTime();
        $.getJSON(url, function(data) {
            for (var i = 0; i < data.length; i++) {
                var $li = $('<li>' + data[i].name + '</li>');
                $li.attr("path", data[i].path);
                $("#filelist").append($li);

                // 点击的时候,获取路径,设置给video的src属性
                $li.click(function() {
                    var p = "/files/" + $(this).attr("path");
                    $video.attr("src", "/files/" + $(this).attr("path"));
                    $video[0].play();
                });
            }
        });
    });

</script>

当然页面上body标签内部也多了一个video标签。

 <video id="videoplayer" controls="controls">
 </video>

到这里,所以的代码就介绍完了~~

小结

回头看,其实就是app中启动服务器,监听一些url,然后针对性的返回文本、json、文件流等。

当然了,可以做的时候也挺多的,甚至可以做个PC版本的文件浏览器。

可能有很多人对html,js不太熟悉,不过还是建议简单了解下,或者敲一下本例,因为本例代码很少,值得作为上手教程。

源码地址:

https://github.com/hongyang钱柜娱乐开户/demo_ShowPhoneMp4

欢迎关注我的新网站:
http://www.wan钱柜娱乐开户.com/index

作者:lmj623565791 发表于 2018/01/17 09:48:14 原文链接 /lmj623565791/article/details/79081656
阅读:5441 评论:17 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/78714705 /lmj623565791/article/details/78714705 lmj623565791 2017/12/05 09:45:02

本文已在我的公众号hongyang钱柜娱乐开户原创首发。
转载请标明出处:
/lmj623565791/article/details/78714705
本文出自张鸿洋的博客

一、概述

貌似前段时间刷知乎看到的一种非常有特色的广告展现方式,即在列表页,某一个Item显示背后部分广告图,随着列表滚动,会逐渐展示全部图片。

刚看到的时候就想实现一哈,一直比较懒,公众号后台也有人问如何实现,今天来给大家讲解下,当然了,目前一些自定义View已经不算难题,所以本文的讲解会做一些实现思路引导,相信不会是那么枯燥的文章,希望对大家有一定的帮助。

恩,现在知乎上已经找不到该效果了,试了多个历史版本也没找到,那只能贴实现的效果图了~

效果图如下:

2选1,你喜欢哪个效果图呢~~

二、思路

好了,抛开别的,确定下本文的目标:

实现在列表中展示某张图片:

往上滚动:在图片刚出现时展示顶部部分,随着滚动部分展示全部
往下滚动:在图片刚出现时展示底部部分,随着滚动部分展示全部

换句话说,我们需要在列表滚动时,改变图片显示的部分。

两个点:

  • 捕获列表滚动的dy,不管是ListView还是RecyclerView相信这一点都能做到
  • 图片显示部分变化,我们可以利用canvas.translate

结合一下,就是,监听列表的滚动dy,传给我们的图片控件,设置translate,然后绘制。

到这里,思路非常清晰,这个东西肯定能做了。

初步方案:自定义一个View,自己去绘制bitmap,对外暴露setDy(dy),然后根据dy做canvas偏移重绘即可。

有了初步方案,基本不慌了,那么再想想?

能否利用已有的控件,比如ImageView呢?

肯定可以,这样省去了我们去声明一个接受图片的属性,我们编写一个子类,依然是通过设置src去使用。

那继承ImageView实现一波再说。

开始码代码前,力推下我的公众号:

专注于钱柜娱乐开户的相关技术~

三、实现

首先我们先写个假的列表,鉴于RV用的越来越多,就用RecyclerView吧。

布局

主布局文件,一个RecyclerView即可:

<?xml version="1.0" encoding="utf-8"?>
<钱柜娱乐开户.support.v7.widget.RecyclerView
    xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户"
    xmlns:app="http://schemas.钱柜娱乐开户.com/apk/res-auto"
    钱柜娱乐开户:id="@+id/id_recyclerview"
    钱柜娱乐开户:layout_width="match_parent"
    钱柜娱乐开户:layout_height="match_parent"
 />

item布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户"
    钱柜娱乐开户:layout_width="match_parent"
    钱柜娱乐开户:layout_height="wrap_content"
    钱柜娱乐开户:background="@drawable/item_bg"
    钱柜娱乐开户:gravity="center">

    <com.imooc.rvimageads.AdImageViewVersion1
        钱柜娱乐开户:id="@+id/id_iv_ad"
        钱柜娱乐开户:layout_width="match_parent"
        钱柜娱乐开户:layout_height="180dp"
        钱柜娱乐开户:scaleType="matrix"
        钱柜娱乐开户:src="@mipmap/grsm"
        钱柜娱乐开户:visibility="gone" />

    <TextView
        钱柜娱乐开户:layout_margin="12dp"
        钱柜娱乐开户:id="@+id/id_tv_title"
        钱柜娱乐开户:layout_width="wrap_content"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:text="这是title"
        钱柜娱乐开户:textSize="16dp"
        钱柜娱乐开户:textStyle="bold" />

    <TextView
        钱柜娱乐开户:id="@+id/id_tv_desc"
        钱柜娱乐开户:layout_width="wrap_content"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:layout_below="@id/id_tv_title"
        钱柜娱乐开户:layout_marginLeft="12dp"
        钱柜娱乐开户:layout_marginRight="12dp"
        钱柜娱乐开户:layout_marginBottom="12dp"
        钱柜娱乐开户:text="这是描述" />

</RelativeLayout>

很简单,先不用管AdImageViewVersion1类,这将是我们具体的实现类。
通过布局文件,可以看到,我们只使用了一个item布局文件,然后通过visible,gone控制展示不同形态。

Activity


public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private LinearLayoutManager mLinearLayoutManager;

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

        mRecyclerView = findViewById(R.id.id_recyclerview);

        List<String> mockDatas = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            mockDatas.add(i + "");
        }

        mRecyclerView.setLayoutManager(mLinearLayoutManager = new LinearLayoutManager(this));

        mRecyclerView.setAdapter(new CommonAdapter<String>(MainActivity.this,
                R.layout.item,
                mockDatas) {
            @Override
            protected void convert(ViewHolder holder, String o, int position) {
                if (position > 0 && position % 6 == 0) {
                    holder.setVisible(R.id.id_tv_title, false);
                    holder.setVisible(R.id.id_tv_desc, false);
                    holder.setVisible(R.id.id_iv_ad, true);
                } else {
                    holder.setVisible(R.id.id_tv_title, true);
                    holder.setVisible(R.id.id_tv_desc, true);
                    holder.setVisible(R.id.id_iv_ad, false);
                }
            }
        });
}

仅仅是设置数据了,Adapter这里用了

compile 'com.zhy:base-rvadapter:3.0.3'

你可以随便用一个你自己喜欢的Adapter封装类。

到这里,一个列表页就显示出来了,并且每隔6个会显示成图片。

不截图了,脑补下…

现在才正式开始实现。

自定义AdImageView

public class AdImageViewVersion1 extends AppCompatImageView {
    public AdImageViewVersion1(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    private RectF mBitmapRectF;
    private Bitmap mBitmap;

    private int mMinDy;

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

        mMinDy = h;
        Drawable drawable = getDrawable();

        if (drawable == null) {
            return;
        }

        mBitmap = drawableToBitamp(drawable);
        mBitmapRectF = new RectF(0, 0,
                w,
                mBitmap.getHeight() * w / mBitmap.getWidth());

    }


    private Bitmap drawableToBitamp(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bd = (BitmapDrawable) drawable;
            return bd.getBitmap();
        }
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, w, h);
        drawable.draw(canvas);
        return bitmap;
    }

    // ... 省略一些代码
}

因为我们要绘制,所以这里我们把drawable转成bitmap,然后我们默认要显示最底部,所以需要一个最小的偏移,即控件高度。

这些事情,我们都在onSizeChanged做了。

并且我们根据当前控件宽度,对bitmap进行了缩放,并将缩放后的尺寸存在了mBitmapRectF中,以便于绘制。

那么接下来就是绘制了,还记得绘制过程中,我们主要利用translate来控制绘制的区域,所以我们还要对外暴露一个setDy方法,so,我们的代码大致是这样的:

private int mDy;

public void setDy(int dy) {

    if (getDrawable() == null) {
        return;
    }
    mDy = dy - mMinDy;
    if (mDy <= 0) {
        mDy = 0;
    }
    if (mDy > mBitmapRectF.height() - mMinDy) {
        mDy = (int) (mBitmapRectF.height() - mMinDy);
    }
    invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
    if (mBitmap == null) {
        return;
    }
    canvas.save();
    canvas.translate(0, -mDy);
    canvas.drawBitmap(mBitmap, null, mBitmapRectF, null);
    canvas.restore();
}

setDy的时候,我们做了一个边界判断,最小的情况,我们偏移-mMinDy,显示图片的底部。
最大的时候,我们便宜图片高度-mMinDy,显示顶部部分。

所以我们对传入的值做了最小与最大值判断。

那么在绘制的时候,就简单了,先translate dy距离,然后绘制即可。

到这里我们的自定义View部分就结束了,代码很少~

结合RecyclerView

接下来就是在RecyclerView滚动时,给我们传入dy即可。

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        int fPos = mLinearLayoutManager.findFirstVisibleItemPosition();
        int lPos = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
        for (int i = fPos; i <= lPos; i++) {
            View view = mLinearLayoutManager.findViewByPosition(i);
            AdImageViewVersion1 adImageView = view.findViewById(R.id.id_iv_ad);
            if (adImageView.getVisibility() == View.VISIBLE) {
                adImageView.setDy(mLinearLayoutManager.getHeight() - view.getTop());
            }
        }
    }
});

通过addOnScrollListener监听,当滚动时,拿到所有可见的Item,找出正在显示图片的Item。然后调用setDy,dy的值为mLinearLayoutManager.getHeight() - view.getTop(),当View从最底部出现的时候为0,当View到达最顶部的时候为当前rv的高度。

你可以合理的利用setDy传入的值,做移动差,显示区域从上到下等,都可以。

这样就完成了~~

一句话实现:即滚动时不断改变dy,然后translate绘制即可。

四、再想想

看着这个代码,好像drawableToBitamp看起来非常不爽,也是比较耗内存的部分。我们再想想:

本身Drawable就是能绘制的,为什么我们要转成bitmap呢?

好像有道理,ImageView本身绘制的就是Drawable,我们需要控制的就是这个Drawable的绘制范围要足够大,不能被控件本身的宽高所影响,导致图片被压扁。

好像有那么一个方法:

drawable.setBounds();

那就简单了,去除drawable2bitmap的代码,直接利用原本的绘制即可,我们唯一要做的就是设置bounds,做一个translate dy即可。

完整代码:

public class AdImageView extends AppCompatImageView {
    // 删除构造方法

    private int mDx;
    private int mMinDx;

    public void setDx(int dx) {
        if (getDrawable() == null) {
            return;
        }
        mDx = dx - mMinDx;
        if (mDx <= 0) {
            mDx = 0;
        }
        if (mDx > getDrawable().getBounds().height() - mMinDx) {
            mDx = getDrawable().getBounds().height() - mMinDx;
        }
        invalidate();
    }

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

    public int getDx() {
        return mDx;
    }

    @Override
    protected void onDraw(Canvas canvas) {

        Drawable drawable = getDrawable();
        int w = getWidth();
        int h = (int) (getWidth() * 1.0f / drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight());
        drawable.setBounds(0, 0, w, h);
        canvas.save();
        canvas.translate(0, -getDx());
        super.onDraw(canvas);
        canvas.restore();
    }
}

短短的代码就实现了,这样看起来顺眼多了~~

再贴下效果图:

效果图主要看字,你懂的!

好了,本篇总结:

看到当看一个效果,可以先对它进行拆分,找出关键点,针对每个关键点,考虑可行性。

如果确定每个点都可行,那么基本的方案就出来了。

有了基本的方案,不要着急写,再想想还有无改善空间。

例子比较简单,have a nice day ~~

https://github.com/hongyang钱柜娱乐开户/demo_rvadimage

作者:lmj623565791 发表于 2017/12/05 09:45:02 原文链接 /lmj623565791/article/details/78714705
阅读:7837 评论:25 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/78011599 /lmj623565791/article/details/78011599 lmj623565791 2017/09/17 17:06:43

本文已在我的公众号hongyang钱柜娱乐开户原创首发。
转载请标明出处:
/lmj623565791/article/details/78011599
本文出自张鸿洋的博客

本文已在我的公众号hongyang钱柜娱乐开户原创首发,文章合集

一、概述

ConstraintLayout出现有一段时间了,不过一直没有特别去关注,也多多少少看了一些文字介绍,多数都是对使用可视化布局拖拽,个人对拖拽一直不看好,直到前段时间看到该文:

非常详尽的介绍了ConstraintLayout的性能优势,于是乎开始学习了一下ConstraintLayout。

本文的重点不在与可视化界面的学习,而在于如何手写各类约束布局属性。对于可视化界面学习推荐:

下面开始进入正题,大家都知道,当布局嵌套深入比较深的时候,往往会伴随着一些性能问题。所以很多时候我们建议使用RelativeLayout或者GridLayout来简化掉布局的深度。

而对于简化布局深度,ConstraintLayout几乎可以做到极致,接下来我们通过实例来尽可能将所有常见的属性一步步的介绍清楚。

首先需要引入我们的ConstraintLayout,在build.gradle中加入:

compile 'com.钱柜娱乐开户.support.constraint:constraint-layout:1.0.2'

二、来编写一个Feed Item

我们先看一个简单的新闻列表中常见的feed item。

看到这样的布局,大家条件反射应该就是使用RelativeLayout来做,当然了,本案例我们使用ConstraintLayout来写:

<钱柜娱乐开户.support.constraint.ConstraintLayout 
    xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户"
    xmlns:app="http://schemas.钱柜娱乐开户.com/apk/res-auto"
    xmlns:tools="http://schemas.钱柜娱乐开户.com/tools"
    钱柜娱乐开户:id="@+id/activity_main"
    钱柜娱乐开户:layout_width="match_parent"
    钱柜娱乐开户:layout_height="match_parent"
    钱柜娱乐开户:background="#11ff0000"
    tools:context="com.zhy.constrantlayout_learn.MainActivity">


    <TextView
        钱柜娱乐开户:id="@+id/tv1"
        钱柜娱乐开户:layout_width="140dp"
        钱柜娱乐开户:layout_height="86dp"
        钱柜娱乐开户:layout_marginLeft="12dp"
        钱柜娱乐开户:layout_marginTop="12dp"
        钱柜娱乐开户:background="#fd3"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

    <TextView
        钱柜娱乐开户:id="@+id/tv2"
        钱柜娱乐开户:layout_width="0dp"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:layout_marginLeft="8dp"
        钱柜娱乐开户:layout_marginRight="12dp"
        钱柜娱乐开户:text="马云:一年交税170多亿马云:一年交税170多亿马云:一年交税170多亿"
        钱柜娱乐开户:textColor="#000000"
        钱柜娱乐开户:textSize="16dp"
        app:layout_constraintLeft_toRightOf="@id/tv1"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/tv1" />

    <TextView
        钱柜娱乐开户:layout_width="wrap_content"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:layout_marginLeft="8dp"
        钱柜娱乐开户:layout_marginTop="12dp"
        钱柜娱乐开户:text="8分钟前"
        钱柜娱乐开户:textColor="#333"
        钱柜娱乐开户:textSize="12dp"
        app:layout_constraintLeft_toRightOf="@id/tv1"
        app:layout_constraintBottom_toBottomOf="@id/tv1" />

</钱柜娱乐开户.support.constraint.ConstraintLayout>

看上面的布局,我们好像看到了几个模式的属性:

首先是tv1,有两个没见过的属性:

  • app:layout_constraintLeft_toLeftOf="parent"

从字面上看,指的是让该控件的左侧与父布局对齐,当我们希望控件A与控件B左侧对齐时,就可以使用该属性。

app:layout_constraintLeft_toLeftOf="@id/viewB"

类似的还有个相似的属性为:

  • app:layout_constraintLeft_toRightOf

很好理解,即当前属性的左侧在谁的右侧,当我们希望控件A在控件B的右侧时,可以设置:

app:layout_constraintLeft_toRightOf="@id/viewB"

与之类似的还有几个属性:

  • layout_constraintRight_toLeftOf
  • layout_constraintRight_toRightOf
  • layout_constraintTop_toTopOf
  • layout_constraintTop_toBottomOf
  • layout_constraintBottom_toTopOf
  • layout_constraintBottom_toBottomOf
  • layout_constraintBaseline_toBaselineOf

类推就可以了。

现在在头看刚才的布局:

tv1设置了:

app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"

tv2设置了:

app:layout_constraintLeft_toRightOf="@id/tv1"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/tv1"

tv3设置了:

app:layout_constraintLeft_toRightOf="@id/tv1"
app:layout_constraintBottom_toBottomOf="@id/tv1"

按照我们刚才的理解,再次的解读下:

tv1应该是在父布局的左上角;

tv2在tv1的右侧,tv2的右侧和父布局对其,tv2和tv1顶部对齐;

tv3在tv1的右侧,tv3和tv1底部对其。

到这里,大家可以看到,目前我们已经可以控制任何一个控件与其他控件间的相对位置了,以及与parent间的相对位置。

和RL的差异

大家是不是觉得目前来看和RelativeLayout特别像?

其实还是有很明显的区别的,我们通过一个例子来看一下:

<RelativeLayout xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户"
    钱柜娱乐开户:layout_width="match_parent" 钱柜娱乐开户:layout_height="match_parent">

    <Button
        钱柜娱乐开户:id="@+id/id_btn01"
        钱柜娱乐开户:layout_width="100dp"
        钱柜娱乐开户:text="Btn01"
        钱柜娱乐开户:layout_height="wrap_content" />

    <Button
        钱柜娱乐开户:layout_width="wrap_content"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:layout_toRightOf="@id/id_btn01"
        钱柜娱乐开户:text="Btn02"
        钱柜娱乐开户:layout_alignParentRight="true"
        />

</RelativeLayout>

那么经过我们刚才的学习,把:

layout_toRightOf="@id/id_btn01"layout_alignParentRight="true"

分别替换为:

app:layout_constraintLeft_toRightOf="@id/id_btn01"app:layout_constraintRight_toRightOf="parent"

是不是觉得so easy ,但是我们看一下效果图:

是不是和预期有一定的区别,假设你将Btn02的宽度设置的非常大,你会发现更加诡异的事情:

你会发现Btn02,好像疯了一样,我们设置的在btn01右侧,和与parent右侧对齐完全失效了!!!

别怕,接下来就让你认识到为什么这个控件叫做“Constraint”Layout。

在当控件有自己设置的宽度,例如warp_content、固定值时,我们为控件添加的都是约束“Constraint”,这个约束有点像橡皮筋一样会拉这个控件,但是并不会改变控件的尺寸(RL很明显不是这样的)。

例如上例,当btn02的宽度较小时,我们为其左侧设置了一个约束(btn01右侧),右侧设置了一个约束(parent右侧对其),当两个约束同时生效的时候(你可以认为两边都是相同的一个拉力),btn02会居中。

当btn02特别大的时候,依然是这两个力,那么会发生什么?会造成左侧和右侧超出的距离一样大。

那么现在大家肯定有些疑问:

  • 怎么样才能和上面的RL一样,宽度刚好占据剩下的距离呢(btn01右侧到屏幕右侧的距离)?

这个问题,问得很好,我们刚才所有的尝试都是在控件自身拥有特定的宽度情况下执行的;那么如果希望控件的宽度根据由约束来控件,不妨去掉这个特定的宽度,即设置为0试试?

对!当我们将btn02的宽度设置为0时,一切又变得很完美。

那么这里,你可能会问0值是什么含义,其实在ConstraintLayout中0代表:MATCH_CONSTRAINT,看到这个常量,是不是瞬间觉得好理解了一点。

  • 最后一个问题,MATCH_PARENT哪去了?

看官网的解释:

Important: MATCH_PARENT is not supported for widgets contained in a ConstraintLayout, though similar behavior can be defined by using MATCH_CONSTRAINT with the corresponding left/right or top/bottom constraints being set to “parent”.`

所以你可以认为:在ConstraintLayout中已经不支持MATCH_PARENT这个值了,你可以通过MATCH_CONSTRAINT配合约束实现类似的效果。

好了,到这里,目前我们已经看到其已经和RelativeLayout势均力敌了,接下来我们看一下RL做不到的特性。

三、增加一个banner

我们现在以往在这个feed item顶部添加一个banner,宽度为占据整个屏幕,宽高比为16:6。

这里尴尬了,在之前的做法,很难在布局中设置宽高比,一般我们都需要在代码中显示的去操作,那么如果你用了ConstraintLayout,它就支持。

看一眼如何支持:

<钱柜娱乐开户.support.constraint.ConstraintLayout 
    ...
    tools:context="com.zhy.constrantlayout_learn.MainActivity">

    <TextView
        钱柜娱乐开户:id="@+id/banner"
        钱柜娱乐开户:layout_width="0dp"
        钱柜娱乐开户:layout_height="0dp"
        钱柜娱乐开户:background="#765"
        钱柜娱乐开户:gravity="center"
        钱柜娱乐开户:text="Banner"
        app:layout_constraintDimensionRatio="H,16:6"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />


    <TextView
        钱柜娱乐开户:id="@+id/tv1"
        app:layout_constraintTop_toBottomOf="@id/banner"
        ></TextView>
     ...
</...>

我们添加了一个banner,还记得我们刚才所说的么,不要使用match_parent了,而是设置match_contraint,即0,让约束来控制布局宽高。

所以我们设置了宽、高都是match_contraint,然后这两个属性:

app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

让我们的宽度充满整个父布局,在添加一个:

app:layout_constraintDimensionRatio="16:6"

该属性指的是宽高比,所以16:6就可以完成我们的需求。

好了看下效果图:

这个宽高比属性,还支持这样的写法:

app:layout_constraintDimensionRatio="W,16:6"
app:layout_constraintDimensionRatio="H,16:6"

可以自己试验下。

好了,到这里,我们又新增了一个属性,还是个非常实用的属性。

那么,我们继续,再看一个似曾相识的功能。

四、增加几个Tab

现在我们希望在底部增加3个tab,均分。是不是想到了LinearLayout和weight。

没错!ConstraintLayout也支持类似的属性。

虽然我知道,但是写到这我还是有点小惊喜~~

看下如何实现:

<TextView
    钱柜娱乐开户:id="@+id/tab1"
    钱柜娱乐开户:layout_width="0dp"
    钱柜娱乐开户:layout_height="30dp"
    钱柜娱乐开户:background="#f67"
    钱柜娱乐开户:gravity="center"
    钱柜娱乐开户:text="Tab1"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/tab2" />


<TextView
    钱柜娱乐开户:id="@+id/tab2"
    钱柜娱乐开户:layout_width="0dp"
    钱柜娱乐开户:layout_height="30dp"
    钱柜娱乐开户:background="#A67"
    钱柜娱乐开户:gravity="center"
    钱柜娱乐开户:text="Tab2"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toRightOf="@id/tab1"
    app:layout_constraintRight_toLeftOf="@+id/tab3" />


<TextView
    钱柜娱乐开户:id="@+id/tab3"
    钱柜娱乐开户:layout_width="0dp"
    钱柜娱乐开户:layout_height="30dp"
    钱柜娱乐开户:background="#767"
    钱柜娱乐开户:gravity="center"
    钱柜娱乐开户:text="Tab3"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toRightOf="@id/tab2"
    app:layout_constraintRight_toRightOf="parent" />

我们增加3个textview来冒充tab。我们看横向的依赖,3个tab两两设置了约束(即你在我们的左边,我在你的右边),最外层的设置了parent约束;再加上我们把宽度都设置为了match_constraint,so,这样我们就完成了3个tab等分。

看一眼效果图:

你可能会说,LL配合weight更加灵活,可以单个设置占据的比例。

对,没错,我们也支持,我不是还没说完么。

现在我们可以给每个tab设置一个属性:

app:layout_constraintHorizontal_weight

看到这个名字,应该就明白了吧,假设我们分别设置值为2,1,1。

效果图为:

是不是很惊喜,别急,刚才你说我不如LL,现在我要让你再看一些LL配合weight做不到的。

这里需要借助几张官网上的图了:

刚才我们说了,3个tab两两设置了依赖,即类似下图:

横向的相当于组成了一个链(Chains)。在这个链的最左侧的元素成为链头,我们可以在其身上设置一些属性,来决定这个链的展示效果:

该属性为:

layout_constraintHorizontal_chainStyle

我们已经见过一种效果了,即按照weight等分,可以成为weighted chain。设置条件为:

chainStyle=”spread”,所有控件宽度设置为match_constraint,因为默认就是spread,所以我们没有显示设置。

其取值还可以为:

  • packed
  • spread_inside

我还是分别显示一下吧:

  1. spread + 宽度非0

  1. spread + 宽度为0,且可以通过weight控制分配比例(上例)

  2. spread_inside + 宽度非0

  1. packed + 宽度非0

好了,差不多了,我们可以在横向或者纵向组成一个Chain,然后在Chain head设置chainStyle来搞一些事情。

官网有个图:

前四个我们都演示了,最后一个设计到一个新的bias属性,别急,咱们慢慢说~~

好了,到这里,我们再次见证了ConstraintLayout的强大。

我们最后再看一个例子。

五、增加浮动按钮

一个很常见的功能,我们现在希望在右下角增加一个浮动按钮。

看下如何实现:

<钱柜娱乐开户.support.constraint.ConstraintLayout 
    ...
    tools:context="com.zhy.constrantlayout_learn.MainActivity">

    <TextView
        钱柜娱乐开户:layout_width="60dp"
        钱柜娱乐开户:layout_height="60dp"
        钱柜娱乐开户:background="#612"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.9" />

</....>

我们在最后追加一个TextView冒充我们的浮动按钮。可以看到我们设置了固定值,被设置约束为右下角。

正常情况我们可以通过margin来设置与右侧与底部的距离。

但是这里我们尝试使用量个新的属性:

layout_constraintHorizontal_bias
layout_constraintVertical_bias

即设置上下两侧间隙比例分别为90%与10%。这个很好理解,我们之前说了,再没有bias这个属性的时候,这两侧的拉力大小是一样的,但是你可以通过bias来控制哪一侧的力要大一些~~明白了么~

所以,该属性可以用于约束之前,控制两侧的“拉力”。

我们看一下效果图:

那么到这里,ConstraintLayout的属性我们基本上介绍完了:

我们看一下:

layout_constraintLeft_toLeftOf 
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf

# 即文章的baseline对齐
layout_constraintBaseline_toBaselineOf

# 与left,right类似
layout_constraintStart_toEndOf 
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf

# margin不需要解释
钱柜娱乐开户:layout_marginStart
钱柜娱乐开户:layout_marginEnd
钱柜娱乐开户:layout_marginLeft
钱柜娱乐开户:layout_marginTop
钱柜娱乐开户:layout_marginRight
钱柜娱乐开户:layout_marginBottom

layout_constraintHorizontal_bias  
layout_constraintVertical_bias  

layout_constraintHorizontal_chainStyle
layout_constraintVertical_chainStyle

layout_constraintVertical_weight

Guideline 

好像,还有个比较特殊的,叫Guideline。

好吧,继续~

六、尝试使用Guideline

钱柜娱乐开户.support.constraint.Guideline该类比较简单,主要用于辅助布局,即类似为辅助线,横向的、纵向的。该布局是不会显示到界面上的。

所以其有个属性为:

钱柜娱乐开户:orientation取值为”vertical”和”horizontal”.

除此以外,还差个属性,决定该辅助线的位置:

  • layout_constraintGuide_begin
  • layout_constraintGuide_end
  • layout_constraintGuide_percent

可以通过上面3个属性其中之一来确定属性值位置。

begin=30dp,即可认为距离顶部30dp的地方有个辅助线,根据orientation来决定是横向还是纵向。

end=30dp,即为距离底部。
percent=0.8即为距离顶部80%。

好了,下面看一个例子,刚才我们的浮点按钮,我决定通过两根辅助线来定位,一根横向距离底部80%,一个纵向距离顶部80%,浮点按钮就定位在他们交叉的地方。

<钱柜娱乐开户.support.constraint.ConstraintLayout 
    ...
    tools:context="com.zhy.constrantlayout_learn.MainActivity">

    <钱柜娱乐开户.support.constraint.Guideline
        钱柜娱乐开户:id="@+id/guideline_h"
        钱柜娱乐开户:layout_width="wrap_content"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:orientation="horizontal"
        app:layout_constraintGuide_percent="0.8" />


    <钱柜娱乐开户.support.constraint.Guideline
        钱柜娱乐开户:id="@+id/guideline_w"
        钱柜娱乐开户:layout_width="wrap_content"
        钱柜娱乐开户:layout_height="wrap_content"
        钱柜娱乐开户:orientation="vertical"
        app:layout_constraintGuide_percent="0.8" />

    <TextView
        钱柜娱乐开户:layout_width="60dp"
        钱柜娱乐开户:layout_height="60dp"
        钱柜娱乐开户:background="#612"
        app:layout_constraintLeft_toRightOf="@id/guideline_w"
        app:layout_constraintTop_toBottomOf="@id/guideline_h" />

</....>

我感觉都不用解释了~~看眼效果图吧:

到此,属性基本上讲完啦~

可以看到,上述相当复杂的一个布局,在ConstraintLayout中完全没有嵌套!

六、总结

本文通过实际的按钮,基本上介绍了ConstraintLayout所支持的所有的属性,全文没有提及拖拽,因为当界面复杂之后,想要完美的拖拽实在是太难了,而且谁也不期望,看不懂拖拽完成后的布局属性吧~

所以,我建议还是尽可能手写,通过本文这样一个流程,虽然支持的属性有20多个,但是分类后并不难记,难记也可以拿出本文翻一翻~

好了,思考了半天,如何通过一个案例介绍完所有的属性,总体来说还是完成了,给自己点个赞。


支持我的话可以关注下我的公众号,每天都会推送新知识~

欢迎关注我的微信公众号:hongyang钱柜娱乐开户
(可以给我留言你想学习的文章,支持投稿)

参考

作者:lmj623565791 发表于 2017/09/17 17:06:43 原文链接 /lmj623565791/article/details/78011599
阅读:26601 评论:52 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/77937483 /lmj623565791/article/details/77937483 lmj623565791 2017/09/12 07:53:43

本文已在我的公众号hongyang钱柜娱乐开户原创首发。
转载请标明出处:
/lmj623565791/article/details/77937483
本文出自张鸿洋的博客

本文已在我的公众号hongyang钱柜娱乐开户原创首发,文章合集

公众号后台很多关注者给我留言,想学习直播相关技术,但是无从下手,其实我也非直播专业人士,不过可以提供点入门的方案,希望以此做到一定的引导作用。

首先搜索了一波,发现了知乎上还有个类似的提问:

https://www.zhihu.com/question/49160322/answer/114587604

文章中第一个回答就是指导如何搭建一个直播系统。

从0开始搭建一个直播系统

我立马实践了下,所以首先给大家分享下整个搭建的流程:

本人的操作系统为mac,其他系统的同学可以根据提示,自行安装软件。

一个简易的直播系统,大致可以由三部分组成:

  • 搭建一个rtmp媒体服务器
  • 推流端
  • 拉流端

现在目标是快速搭建起来,所以当然是借助开源项目和一些软件:

  • rtmp媒体服务器:这里使用srs
  • 推流端:这里使用obs
  • 拉流端:这里使用播放器vlc

rtmp媒体服务器的搭建

这里使用srs,srs的链接为:
https://github.com/ossrs/srs

首先clone到本地,进入到trunk目录:

git clone https://github.com/ossrs/srs.git
cd srs/trunk

然后执行:

./configure --osx

注意: Centos6.x/Ubuntu12 32/64bits用户仅需要执行./configure。

最后执行:

make

执行成功后,就可以开启我们的服务了:

./etc/init.d/srs start

如果是mac系统,此时会失败,原因是srs.conf中max_connections太大,
目录为srs/trunk/conf/srs.conf,可以修改为248(其他操作系统可能无此问题)。

再次回到trunk目录:

./etc/init.d/srs start

到此我们的srs服务器就搭建起来了。

注:
Centos、Ubuntu可以参考官网搭建,比较简单。
如果你启动过程中还遇到了其他错误,可以查看log信息:
srs/trunk/objs/srs.log
其他指令:
停止 ./etc/init.d/srs stop
重启 ./etc/init.d/srs restart

有了服务器之后,我们就准备开始我们的推流端。

如果你实在搭建不成功,可以先拿116.196.121.20这个ip做测试,我在京东云上搭建的,配置较低,主要用于大家临时测试,可能不稳定,看一眼就行,后续会关掉,所以还是尽可能自己搭建成功吧。

使用OBS推流

下载地址:https://obsproject.com/

先下载安装,这里就简单了

首先选择点击+选择来源,这里我选择了窗口捕获,然后点击右侧的设置:

选择流,串流类型选择自定义,然后url,填写:

rtmp://你的ip/你喜欢的url

流名称可以按照上述自由输入。

记住我们的url和流名称:

rtmp://192.168.1.102/zhy/mylive

完成后,点击确定。

然后点击开始推流即可。

这样,我们的OBS推流就开启啦,软件的更多使用自行探索吧。

最后就剩下我们的拉流了。

使用VLC拉流

下载地址:http://www.videolan.org/vlc/

先下载安装,这个就更简单啦。

点击Open Network,输入我们刚才的url+流名称,点击确定即可。

稍等,就开始播放我们的推流内容了。

演示个动图:

最左侧是VLC,中间是OBS,右侧是我窗口捕获对象。

到这里,就算我们搭建了一个直播系统啦~~自己搭建成功的感觉,无比爽快,也能很大的激发我们后续的学习兴趣。

后面我们可以根据自己的需求去选择学习拉流或者推流,逐步替换掉软件。拉流的方式很多,很多开源播放器都支持。这里我们考虑替换掉推流,希望可以使用手机来推流。

使用第三方推流SDK

最简单的方式,就是可以借助于第三方的推流SDK,大多情况下第三方SDK完整方案都是收费的,不过他们的推流钱柜娱乐开户 SDK倒是可以下载无需付费情况下来学习使用的。

这里以百度云的直播SDK为例,下载地址:
https://cloud.baidu.com/doc/Downloadcenter/Push.html#.E7.89.88.E6.9C.AC.E6.9B.B4.E6.96.B0.E8.AF.B4.E6.98.8E

直接点击下载钱柜娱乐开户 SDK即可,下载完成后解压,然后倒入AS(这竟然是个Eclipse项目…),还好AS支持,导入后:

直接将mStreamKey修改我们的rtmp的地址即可。

注意,需要在build.gradle中添加下v7的依赖

compile 'com.钱柜娱乐开户.support:appcompat-v7:23.0.0'

然后运行,界面还可以:

贴一下运行时的效果图:

还是以vlc拉流即可,整个过程很缓慢,耐心等待,效果也不是很好,不过能跑通即可,主要是学习。然后你可以举一反三试试其他的SDK。

当然了,很多开源项目其实比SDK作为学习资料更好,比较源码都有,下面以一个开源项目举例。

使用开源项目推流

使用一个开源项目:

https://github.com/begeekmyfriend/yasea

直接clone,导入。
这个比较顺利,导入后,修改下rtmp链接:

然后运行即可(导入不成功,自己想办法解决,基础能力啦~)。

贴一张效果图:

硬解码的情况下,效果比前面的SDK好很多~~

好了,最后我们再看一种方式。

恩,ffmpeg很火,ffmpeg很强大。
所以最后一种方式,就是看如何利用ffmpeg推流啦~~

利用ffmpeg推流

大家可以自己下载ffmepg的源码,然后按照网上的方式去编成so,简单的一点而且比较实用的,就是编出可以执行ffmpeg 命令的so,这样就能干很多事情了。

这里,由于篇幅,我们就直接使用别人编好的项目了。

https://github.com/WritingMinds/ffmpeg-钱柜娱乐开户-java

直接导入,该项目支持直接运行ffmpeg的命令。

ffmpeg命令很多:
例如:

将.avi转成gif动画(未压缩)
ffmpeg -i video_origine.avi gif_anime.gif
合成视频和音频
ffmpeg -i son.wav -i video_origine.avi video_finale.mpg
还有非常多的功能,可以参考:
/king1425/article/details/70348374

其中有一个命令就是支持推流,这里将手机上的zixia.mp4作为输入:

ffmpeg -re -i /storage/emulated/0/zixia.mp4 
    -vcodec libx264 
    -acodec aac 
    -f flv 
    -strict -2 rtmp://192.168.1.102/zhy/mylive=

那么这个库是支持在手机上运行ffmpeg命令的,那就简单了:

贴上我们需要执行的命令,运行即可。

这里注意我推的是存储卡上的一个媒体文件,注意添加相关权限,效果如下。

好了,这样我们的大致学习了如何搭建一个小直播系统,如何利用SDK,开源项目,以及简单的使用ffmpeg来进行推流~~

很多时候,学习一个比较大的技术方向就是开头难,无从下手,那么本篇应该是一篇非常易懂的教程,希望对想要学习直播技术的小伙伴有所帮助,也希望以此能够激发大家一定的学习兴趣,当然直播技术远不止于此,大家可以根据自己的情况继续深入学习~


支持我的话可以关注下我的公众号,每天都会推送新知识~

欢迎关注我的微信公众号:hongyang钱柜娱乐开户
(可以给我留言你想学习的文章,支持投稿)

参考:

https://github.com/WritingMinds/ffmpeg-钱柜娱乐开户-java
https://github.com/begeekmyfriend/yasea
https://cloud.baidu.com/doc/Downloadcenter/Push.html
https://www.zhihu.com/question/49160322/answer/114587604
https://github.com/ossrs/srs
http://www.jianshu.com/p/dd3f58392aa0#
/king1425/article/details/70348374

作者:lmj623565791 发表于 2017/09/12 07:53:43 原文链接 /lmj623565791/article/details/77937483
阅读:14014 评论:22 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/75000580 /lmj623565791/article/details/75000580 lmj623565791 2017/07/12 00:03:04

本文已在我的公众号hongyang钱柜娱乐开户原创首发。
转载请标明出处:
/lmj623565791/article/details/75000580
本文出自张鸿洋的博客

本文已在我的公众号hongyang钱柜娱乐开户原创首发,文章合集

一、概述

之前一直没有写过插件化相关的博客,刚好最近滴滴和360分别开源了自家的插件化方案,赶紧学习下,写两篇博客,第一篇是滴滴的方案:

那么其中的难点很明显是对四大组件支持,因为大家都清楚,四大组件都是需要在钱柜娱乐开户Manifest中注册的,而插件apk中的组件是不可能预先知晓名字,提前注册中宿主apk中的,所以现在基本都采用一些hack方案类解决,VirtualAPK大体方案如下:

  • Activity:在宿主apk中提前占几个坑,然后通过“欺上瞒下”(这个词好像是360之前的ppt中提到)的方式,启动插件apk的Activity;因为要支持不同的launchMode以及一些特殊的属性,需要占多个坑。
  • Service:通过代理Service的方式去分发;主进程和其他进程,VirtualAPK使用了两个代理Service。
  • BroadcastReceiver:静态转动态
  • ContentProvider:通过一个代理Provider进行分发。

这些占坑的数量并不是固定的,比如Activity想支持某个属性,该属性不能动态设置,只能在Manifest中设置,那就需要去占坑支持。所以占坑数量这些,可以根据自己的需求进行调整。

下面就逐一去分析代码啦~

注:本篇博客涉及到的framework逻辑,为API 22.

分期版本为 com.didi.virtualapk:core:0.9.0

二、Activity的支持

这里就不按照某个流程一行行代码往下读了,针对性的讲一些关键流程,可能更好阅读一些。

首先看一段启动插件Activity的代码:

final String pkg = "com.didi.virtualapk.demo";
if (PluginManager.getInstance(this).getLoadedPlugin(pkg) == null) {
    Toast.makeText(this, "plugin [com.didi.virtualapk.demo] not loaded", Toast.LENGTH_SHORT).show();
    return;
}

// test Activity and Service
Intent intent = new Intent();
intent.setClassName(pkg, "com.didi.virtualapk.demo.aidl.BookManagerActivity");
startActivity(intent);

可以看到优先根据包名判断该插件是否已经加载,所以在插件使用前其实还需要调用

pluginManager.loadPlugin(apk);

加载插件。

这里就不赘述源码了,大致为调用PackageParser.parsePackage解析apk,获得该apk对应的PackageInfo,资源相关(AssetManager,Resources),DexClassLoader(加载类),四大组件相关集合(mActivityInfos,mServiceInfos,mReceiverInfos,mProviderInfos),针对Plugin的PluginContext等一堆信息,封装为LoadedPlugin对象。

详细可以参考com.didi.virtualapk.internal.LoadedPlugin类。

ok,如果该插件以及加载过,则直接通过startActivity去启动插件中目标Activity。

(1)替换Activity

这里大家肯定会有疑惑,该Activity必然没有在Manifest中注册,这么启动不会报错吗?

正常肯定会报错呀,所以我们看看它是怎么做的吧。

跟进startActivity的调用流程,会发现其最终会进入Instrumentation的execStartActivity方法,然后再通过ActivityManagerProxy与AMS进行交互。

而Activity是否存在的校验是发生在AMS端,所以我们在于AMS交互前,提前将Activity的ComponentName进行替换为占坑的名字不就好了么?

这里可以选择hook Instrumentation,或者ActivityManagerProxy都可以达到目标,VirtualAPK选择了hook Instrumentation.

打开PluginManager可以看到如下方法:

private void hookInstrumentationAndHandler() {
    try {
        Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
        if (baseInstrumentation.getClass().getName().contains("lbe")) {
            // reject executing in paralell space, for example, lbe.
            System.exit(0);
        }

        final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
        Object activityThread = ReflectUtil.getActivityThread(this.mContext);
        ReflectUtil.setInstrumentation(activityThread, instrumentation);
        ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
        this.mInstrumentation = instrumentation;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

可以看到首先通过反射拿到了原本的Instrumentation对象,拿的过程是首先拿到ActivityThread,由于ActivityThread可以通过静态变量sCurrentActivityThread或者静态方法currentActivityThread()获取,所以拿到其对象相当轻松。拿到ActivityThread对象后,调用其getInstrumentation()方法,即可获取当前的Instrumentation对象。

然后自己创建了一个VAInstrumentation对象,接下来就直接反射将VAInstrumentation对象设置给ActivityThread对象即可。

这样就完成了hook Instrumentation,之后调用Instrumentation的任何方法,都可以在VAInstrumentation进行拦截并做一些修改。

这里还hook了ActivityThread的mH类的Callback,暂不赘述。

刚才说了,可以通过Instrumentation的execStartActivity方法进行偷梁换柱,所以我们直接看对应的方法:

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
    // null component is an implicitly intent
    if (intent.getComponent() != null) {
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                intent.getComponent().getClassName()));
        // resolve intent with Stub Activity if needed
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }

    ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                intent, requestCode, options);

    return result;

}

首先调用transformIntentToExplicitAsNeeded,这个主要是当component为null时,根据启动Activity时,配置的action,data,category等去已加载的plugin中匹配到确定的Activity的。

本例我们的写法ComponentName肯定不为null,所以直接看markIntentIfNeeded()方法:

public void markIntentIfNeeded(Intent intent) {
    if (intent.getComponent() == null) {
        return;
    }

    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    // search map and return specific launchmode stub activity
    if (!targetPackageName.equals(mContext.getPackageName())
            && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    }
}

在该方法中判断如果启动的是插件中类,则将启动的包名和Activity类名存到了intent中,可以看到这里存储明显是为了后面恢复用的。

然后调用了dispatchStubActivity(intent)

private void dispatchStubActivity(Intent intent) {
    ComponentName component = intent.getComponent();
    String targetClassName = intent.getComponent().getClassName();
    LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
    ActivityInfo info = loadedPlugin.getActivityInfo(component);
    if (info == null) {
        throw new RuntimeException("can not find " + component);
    }
    int launchMode = info.launchMode;
    Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
    themeObj.applyStyle(info.theme, true);
    String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
    Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
    intent.setClassName(mContext, stubActivity);
}

可以直接看最后一行,intent通过setClassName替换启动的目标Activity了!这个stubActivity是由mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj)返回。

很明显,传入的参数launchMode、themeObj都是决定选择哪一个占坑类用的。

public String getStubActivity(String className, int launchMode, Theme theme) {
    String stubActivity= mCachedStubActivity.get(className);
    if (stubActivity != null) {
        return stubActivity;
    }

    TypedArray array = theme.obtainStyledAttributes(new int[]{
            钱柜娱乐开户.R.attr.windowIsTranslucent,
            钱柜娱乐开户.R.attr.windowBackground
    });
    boolean windowIsTranslucent = array.getBoolean(0, false);
    array.recycle();
    if (Constants.DEBUG) {
        Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
    }
    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
    switch (launchMode) {
        case ActivityInfo.LAUNCH_MULTIPLE: {
            stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
            if (windowIsTranslucent) {
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
            }
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_TOP: {
            usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
            stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
            break;
        }

       // 省略LAUNCH_SINGLE_TASK,LAUNCH_SINGLE_INSTANCE
    }

    mCachedStubActivity.put(className, stubActivity);
    return stubActivity;
}

可以看到主要就是根据launchMode去选择不同的占坑类。
例如:

stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);

STUB_ACTIVITY_STANDARD值为:"%s.A$%d", corePackage值为com.didi.virtualapk.core,usedStandardStubActivity为数字值。

所以最终类名格式为:com.didi.virtualapk.core.A$1

再看一眼,CoreLibrary下的钱柜娱乐开户Manifest中:

<activity 钱柜娱乐开户:name=".A$1" 钱柜娱乐开户:launchMode="standard"/>
<activity 钱柜娱乐开户:name=".A$2" 钱柜娱乐开户:launchMode="standard"
    钱柜娱乐开户:theme="@钱柜娱乐开户:style/Theme.Translucent" />

<!-- Stub Activities -->
<activity 钱柜娱乐开户:name=".B$1" 钱柜娱乐开户:launchMode="singleTop"/>
<activity 钱柜娱乐开户:name=".B$2" 钱柜娱乐开户:launchMode="singleTop"/>
<activity 钱柜娱乐开户:name=".B$3" 钱柜娱乐开户:launchMode="singleTop"/>
// 省略很多...    

就完全明白了。

到这里就可以看到,替换我们启动的Activity为占坑Activity,将我们原本启动的包名,类名存储到了Intent中。

这样做只完成了一半,为什么这么说呢?

(2) 还原Activity

因为欺骗过了AMS,AMS执行完成后,最终要启动的不可能是占坑Activity,还应该是我们的启动的目标Activity呀。

这里需要知道Activity的启动流程:

AMS在处理完启动Activity后,会调用:app.thread.scheduleLaunchActivity,这里的thread对应的server端未我们ActivityThread中的ApplicationThread对象(binder可以理解有一个client端和一个server端),所以会调用ApplicationThread.scheduleLaunchActivity方法,在其内部会调用mH类的sendMessage方法,传递的标识为H.LAUNCH_ACTIVITY,进入调用到ActivityThread的handleLaunchActivity方法->ActivityThread#handleLaunchActivity->mInstrumentation.newActivity()。

ps:这里流程不清楚没关系,暂时理解为最终会回调到Instrumentation的newActivity方法即可,细节可以自己去查看结合老罗的blog理解。

关键的来了,最终又到了Instrumentation的newActivity方法,还记得这个类我们已经改为VAInstrumentation啦:

直接看其newActivity方法:

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
    } catch (ClassNotFoundException e) {
        LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
        String targetClassName = PluginUtil.getTargetActivity(intent);

        if (targetClassName != null) {
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

          // 省略兼容性处理代码
            return activity;
        }
    }

    return mBase.newActivity(cl, className, intent);
}

核心就是首先从intent中取出我们的目标Activity,然后通过plugin的ClassLoader去加载(还记得在加载插件时,会生成一个LoadedPlugin对象,其中会对应其初始化一个DexClassLoader)。

这样就完成了Activity的“偷梁换柱”。

还没完,接下来在callActivityOnCreate方法中:

 @Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    final Intent intent = activity.getIntent();
    if (PluginUtil.isIntentFromPlugin(intent)) {
        Context base = activity.getBaseContext();
        try {
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
            ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
            ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
            ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
            ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());

            // set screenOrientation
            ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
            if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                activity.setRequestedOrientation(activityInfo.screenOrientation);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    mBase.callActivityOnCreate(activity, icicle);
}

设置了修改了mResources、mBase(Context)、mApplication对象。以及设置一些可动态设置的属性,这里仅设置了屏幕方向。

这里提一下,将mBase替换为PluginContext,可以修改Resources、AssetManager以及拦截相当多的操作。

看一眼代码就清楚了:

原本Activity的部分get操作

# ContextWrapper
@Override
public AssetManager getAssets() {
    return mBase.getAssets();
}

@Override
public Resources getResources()
{
    return mBase.getResources();
}

@Override
public PackageManager getPackageManager() {
    return mBase.getPackageManager();
}

@Override
public ContentResolver getContentResolver() {
    return mBase.getContentResolver();
}

直接替换为:

# PluginContext

@Override
public Resources getResources() {
    return this.mPlugin.getResources();
}

@Override
public AssetManager getAssets() {
    return this.mPlugin.getAssets();
}

@Override
public ContentResolver getContentResolver() {
    return new PluginContentResolver(getHostContext());
}

看得出来还是非常巧妙的。可以做的事情也非常多,后面对ContentProvider的描述也会提现出来。

好了,到此Activity就可以正常启动了。

下面看Service。

三、Service的支持

Service和Activity有点不同,显而易见的首先我们也会将要启动的Service类替换为占坑的Service类,但是有一点不同,在Standard模式下多次启动同一个占坑Activity会创建多个对象来对象我们的目标类。而Service多次启动只会调用onStartCommond方法,甚至常规多次调用bindService,seviceConn对象不变,甚至都不会多次回调bindService方法(多次调用可以通过给Intent设置不同Action解决)。

还有一点,最明显的差异是,Activity的生命周期是由用户交互决定的,而Service的声明周期是我们主动通过代码调用的。

也就是说,start、stop、bind、unbind都是我们显示调用的,所以我们可以拦截这几个方法,做一些事情。

Virtual Apk的做法,即将所有的操作进行拦截,都改为startService,然后统一在onStartCommond中分发。

下面看详细代码:

(1) hook IActivityManager

再次来到PluginManager,发下如下方法:

private void hookSystemServices() {
    try {
        Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault");
        IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());

        // Hook IActivityManager from ActivityManagerNative
        ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);

        if (defaultSingleton.get() == activityManagerProxy) {
            this.mActivityManager = activityManagerProxy;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

首先拿到ActivityManagerNative中的gDefault对象,该对象返回的是一个Singleton<IActivityManager>,然后拿到其mInstance对象,即IActivityManager对象(可以理解为和AMS交互的binder的client对象)对象。

然后通过动态代理的方式,替换为了一个代理对象。

那么重点看对应的InvocationHandler对象即可,该代理对象调用的方法都会辗转到其invoke方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("startService".equals(method.getName())) {
        try {
            return startService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Start service error", e);
        }
    } else if ("stopService".equals(method.getName())) {
        try {
            return stopService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop Service error", e);
        }
    } else if ("stopServiceToken".equals(method.getName())) {
        try {
            return stopServiceToken(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop service token error", e);
        }
    }
    // 省略bindService,unbindService等方法
}    

当我们调用startService时,跟进代码,可以发现调用流程为:

startService->startServiceCommon->ActivityManagerNative.getDefault().startService

这个getDefault刚被我们hook,所以会被上述方法拦截,然后调用:startService(proxy, method, args)

private Object startService(Object proxy, Method method, Object[] args) throws Throwable {
    IApplicationThread appThread = (IApplicationThread) args[0];
    Intent target = (Intent) args[1];
    ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
    if (null == resolveInfo || null == resolveInfo.serviceInfo) {
        // is host service
        return method.invoke(this.mActivityManager, args);
    }

    return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}

先不看代码,考虑下我们这里唯一要做的就是通过Intent保存关键数据,替换启动的Service类为占坑类。

所以直接看最后的方法:

private ComponentName startDelegateServiceForTarget(Intent target,
                                                    ServiceInfo serviceInfo,
                                                    Bundle extras, int command) {
    Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
    return mPluginManager.getHostContext().startService(wrapperIntent);
}

最后一行就是启动了,那么替换的操作应该在wrapperTargetIntent中完成:

private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    // fill in service with ComponentName
    target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
    String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

    // start delegate service to run plugin service inside
    boolean local = PluginUtil.isLocalService(serviceInfo);
    Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
    Intent intent = new Intent();
    intent.setClass(mPluginManager.getHostContext(), delegate);
    intent.putExtra(RemoteService.EXTRA_TARGET, target);
    intent.putExtra(RemoteService.EXTRA_COMMAND, command);
    intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
    if (extras != null) {
        intent.putExtras(extras);
    }

    return intent;
}

果不其然,重新初始化了Intent,设置了目标类为LocalService(多进程时设置为RemoteService),然后将原本的Intent存储到EXTRA_TARGET,携带command为EXTRA_COMMAND_START_SERVICE,以及插件apk路径。

(2)代理分发

那么接下来代码就到了LocalService的onStartCommond中啦:


@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 省略一些代码...

    Intent target = intent.getParcelableExtra(EXTRA_TARGET);
    int command = intent.getIntExtra(EXTRA_COMMAND, 0);
    if (null == target || command <= 0) {
        return START_STICKY;
    }

    ComponentName component = target.getComponent();
    LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);

    switch (command) {
        case EXTRA_COMMAND_START_SERVICE: {
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                service = this.mPluginManager.getComponentsHandler().getService(component);
            } else {
                try {
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                } catch (Throwable t) {
                    return START_STICKY;
                }
            }

            service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
            break;
        }
        // 省略下面的代码
         case EXTRA_COMMAND_BIND_SERVICE:break;
         case EXTRA_COMMAND_STOP_SERVICE:break;
         case EXTRA_COMMAND_UNBIND_SERVICE:break;
}

这里代码很简单了,根据command类型,比如EXTRA_COMMAND_START_SERVICE,直接通过plugin的ClassLoader去load目标Service的class,然后反射创建实例。比较重要的是,Service创建好后,需要调用它的attach方法,这里凑够参数,然后反射调用即可,最后调用onCreate、onStartCommand收工。然后将其保存起来,stop的时候取出来调用其onDestroy即可。

bind、unbind以及stop的代码与上述基本一致,不在赘述。

唯一提醒的就是,刚才看到还hook了一个方法叫做:stopServiceToken,该方法是什么时候用的呢?

主要有一些特殊的Service,比如IntentService,其stopSelf是由自身调用的,最终会调用mActivityManager.stopServiceToken方法,同样的中转为STOP操作即可。

四、BroadcastReceiver的支持

这个比较简单,直接解析Manifest后,静态转动态即可。

相关代码在LoadedPlugin的构造方法中:

for (PackageParser.Activity receiver : this.mPackage.receivers) {
    receivers.put(receiver.getComponentName(), receiver.info);

    try {
        BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
        for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
            this.mHostContext.registerReceiver(br, aii);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

可以看到解析到receiver信息后,直接通过pluginClassloader去loadClass拿到receiver对象,然后调用this.mHostContext.registerReceiver即可。

开心,最后一个了~

五、ContentProvider的支持

(1)hook IContentProvider

ContentProvider的支持依然是通过代理分发。

看一段CP使用的代码:

Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);

这里用到了PluginContext,在生成Activity、Service的时候,为其设置的Context都为PluginContext对象。

所以当你调用getContentResolver时,调用的为PluginContext的getContentResolver。

@Override
public ContentResolver getContentResolver() {
    return new PluginContentResolver(getHostContext());
}

返回的是一个PluginContentResolver对象,当我们调用query方法时,会辗转调用到
ContentResolver.acquireUnstableProvider方法。该方法被PluginContentResolver中复写:

protected IContentProvider acquireUnstableProvider(Context context, String auth) {
    try {
        if (mPluginManager.resolveContentProvider(auth, 0) != null) {
            return mPluginManager.getIContentProvider();
        }

        return (IContentProvider) sAcquireUnstableProvider.invoke(mBase, context, auth);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}

如果调用的auth为插件apk中的provider,则直接返回mPluginManager.getIContentProvider()

public synchronized IContentProvider getIContentProvider() {
    if (mIContentProvider == null) {
        hookIContentProviderAsNeeded();
    }

    return mIContentProvider;
}

咦,又看到一个hook方法:

private void hookIContentProviderAsNeeded() {
    Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
    mContext.getContentResolver().call(uri, "wakeup", null, null);
    try {
        Field authority = null;
        Field mProvider = null;
        ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
        Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
        Iterator iter = mProviderMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object val = entry.getValue();
            String auth;
            if (key instanceof String) {
                auth = (String) key;
            } else {
                if (authority == null) {
                    authority = key.getClass().getDeclaredField("authority");
                    authority.setAccessible(true);
                }
                auth = (String) authority.get(key);
            }
            if (auth.equals(PluginContentResolver.getAuthority(mContext))) {
                if (mProvider == null) {
                    mProvider = val.getClass().getDeclaredField("mProvider");
                    mProvider.setAccessible(true);
                }
                IContentProvider rawProvider = (IContentProvider) mProvider.get(val);
                IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                mIContentProvider = proxy;
                Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

前两行比较重要,第一行是拿到了占坑的provider的uri,然后主动调用了其call方法。
如果你跟进去,会发现,其会调用acquireProvider->mMainThread.acquireProvider->ActivityManagerNative.getDefault().getContentProvider->installProvider。简单来说,其首先调用已经注册provider,得到返回的IContentProvider对象。

这个IContentProvider对象是在ActivityThread.installProvider方法中加入到mProviderMap中。

而ActivityThread对象又容易获取,mProviderMap又是它成员变量,那么也容易获取,所以上面的一大坨(除了前两行)代码,就为了拿到占坑的provider对应的IContentProvider对象。

然后通过动态代理的方式,进行了hook,关注InvocationHandler的实例IContentProviderProxy。

IContentProvider能干吗呢?其实就能拦截我们正常的query、insert、update、delete等操作。

拦截这些方法干嘛?

当然是修改uri啦,把用户调用的uri,替换为占坑provider的uri,再把原本的uri作为参数拼接在占坑provider的uri后面即可。

好了,直接看invoke方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Log.v(TAG, method.toGenericString() + " : " + Arrays.toString(args));
    wrapperUri(method, args);

    try {
        return method.invoke(mBase, args);
    } catch (InvocationTargetException e) {
        throw e.getTargetException();
    }
}

直接看wrapperUri

private void wrapperUri(Method method, Object[] args) {
    Uri uri = null;
    int index = 0;
    if (args != null) {
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof Uri) {
                uri = (Uri) args[i];
                index = i;
                break;
            }
        }
    }

    // 省略部分代码

    PluginManager pluginManager = PluginManager.getInstance(mContext);
    ProviderInfo info = pluginManager.resolveContentProvider(uri.getAuthority(), 0);
    if (info != null) {
        String pkg = info.packageName;
        LoadedPlugin plugin = pluginManager.getLoadedPlugin(pkg);
        String pluginUri = Uri.encode(uri.toString());
        StringBuilder builder = new StringBuilder(PluginContentResolver.getUri(mContext));
        builder.append("/?plugin=" + plugin.getLocation());
        builder.append("&pkg=" + pkg);
        builder.append("&uri=" + pluginUri);
        Uri wrapperUri = Uri.parse(builder.toString());
        if (method.getName().equals("call")) {
            bundleInCallMethod.putString(KEY_WRAPPER_URI, wrapperUri.toString());
        } else {
            args[index] = wrapperUri;
        }
    }
}

从参数中找到uri,往下看,搞了个StringBuilder首先加入占坑provider的uri,然后将目标uri,pkg,plugin等参数等拼接上去,替换到args中的uri,然后继续走原本的流程。

假设是query方法,应该就到达我们占坑provider的query方法啦。

(2)代理分发

占坑如下:

<provider
    钱柜娱乐开户:name="com.didi.virtualapk.delegate.RemoteContentProvider"
    钱柜娱乐开户:authorities="${applicationId}.VirtualAPK.Provider"
    钱柜娱乐开户:process=":daemon" />

打开RemoteContentProvider,直接看query方法:

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {

    ContentProvider provider = getContentProvider(uri);
    Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
    if (provider != null) {
        return provider.query(pluginUri, projection, selection, selectionArgs, sortOrder);
    }

    return null;
}

可以看到通过传入的生成了一个新的provider,然后拿到目标uri,在直接调用provider.query传入目标uri即可。

那么这个provider实际上是这个代理类帮我们生成的:

private ContentProvider getContentProvider(final Uri uri) {
    final PluginManager pluginManager = PluginManager.getInstance(getContext());
    Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
    final String auth = pluginUri.getAuthority();
    // 省略了缓存管理
    LoadedPlugin plugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
    if (plugin == null) {
        try {
            pluginManager.loadPlugin(new File(uri.getQueryParameter(KEY_PLUGIN)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);
    if (providerInfo != null) {
        RunUtil.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                    ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();
                    contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);
                    sCachedProviders.put(auth, contentProvider);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, true);
        return sCachedProviders.get(auth);
    }
    return null;
}

很简单,取出原本的uri,拿到auth,在通过加载plugin得到providerInfo,反射生成provider对象,在调用其attachInfo方法即可。

其他的几个方法:insert、update、delete、call逻辑基本相同,就不赘述了。

感觉这里其实通过hook AMS的getContentProvider方法也能完成上述流程,感觉好像可以更彻底,不需要依赖PluginContext了。

六、总结

总结下,其实就是文初的内容,可以看到VritualApk大体方案如下:

  • Activity:在宿主apk中提前占几个坑,然后通过“欺上瞒下”(这个词好像是360之前的ppt中提到)的方式,启动插件apk的Activity;因为要支持不同的launchMode以及一些特殊的属性,需要占多个坑。
  • Service:通过代理Service的方式去分发;主进程和其他进程,VirtualAPK使用了两个代理Service。
  • BroadcastReceiver:静态转动态。
  • ContentProvider:通过一个代理Provider进行分发。

整体代码看起来还是很轻松的~

当然如果你要选择某一个插件化方案进行使用,一定要了解其中的实现原理,文档上描述的并不是所有细节,很多一些属性什么的,以及由于其实现的方式造成一些特性的不支持。了解源码,可以方便自己排查问题,扩展,甚至写一套根据自己业务需求的插件化方案~~

再多嘴一句,还是建议大多多在某一方面深入了解,不要痴迷于UI特效(上班路上看看我的推文就好啦~玩笑~,很多特效的,了解下原理即可)~~其实我早期浪费了很多时间在上面,在你掌握了自定义View的详细细节、事件分发机制这些机制后,大部分UI的编写都是时间问题。

不要在上面浪费过多时间,比别人多研究几个特效并不会对自己的提升有巨大的帮助,过来人,忠言逆耳~。


支持我的话可以关注下我的公众号,每天都会推送新知识~

欢迎关注我的微信公众号:hongyang钱柜娱乐开户
(可以给我留言你想学习的文章,支持投稿)

作者:lmj623565791 发表于 2017/07/12 00:03:04 原文链接 /lmj623565791/article/details/75000580
阅读:22526 评论:45 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/72859156 /lmj623565791/article/details/72859156 lmj623565791 2017/06/09 09:03:46

本文已在我的公众号hongyang钱柜娱乐开户原创首发。
转载请标明出处:
/lmj623565791/article/details/72859156
本文出自张鸿洋的博客

本文已在我的公众号hongyang钱柜娱乐开户原创首发,文章合集

一、概述

之前项目的新特性适配工作都是同事在做,一直没有怎么太关注,不过类似这些适配的工作还是有必要做一些记录的。

对于钱柜娱乐开户 7.0,提供了非常多的变化,详细的可以阅读官方文档钱柜娱乐开户 7.0 行为变更,记得当时做了多窗口支持、FileProvider以及7.1的3D Touch的支持,不过和我们开发者关联最大的,或者说必须要适配的就是去除项目中传递file://类似格式的uri了。

在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException

所以本文主要描述如何适配该问题,没什么难度,仅做记录。

注:本文targetSdkVersion 25 ,compileSdkVersion 25

二、拍照案例

大家应该对于手机拍照一定都不陌生,在希望得到一张高清拍照图的时候,我们通过Intent会传递一个File的Uri给相机应用。

大致代码如下:

private static final int REQUEST_CODE_TAKE_PHOTO = 0x110;
    private String mCurrentPhotoPath;

    public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            mCurrentPhotoPath = file.getAbsolutePath();

            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
            mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
        }
        // else tip?

    }

贴个效果图吧~

未处理6.0权限,有需要的自行处理下,nexus系列如果未处理,需要手动在设置页开启存储权限。

此时如果我们使用钱柜娱乐开户 7.0或者以上的原生系统,再次运行一下,你会发现应用直接停止运行,抛出了钱柜娱乐开户.os.FileUriExposedException

Caused by: 钱柜娱乐开户.os.FileUriExposedException: 
    file:///storage/emulated/0/20170601-030254.png 
        exposed beyond app through ClipData.Item.getUri()
    at 钱柜娱乐开户.os.StrictMode.onFileUriExposed(StrictMode.java:1932)
    at 钱柜娱乐开户.net.Uri.checkFileUriExposed(Uri.java:2348)

所以如果你意识到自己写的代码,在7.0的原生系统的手机上直接就crash是不是很方~

原因在官网已经给了解释:

对于面向 钱柜娱乐开户 7.0 的应用,钱柜娱乐开户 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

同样的,官网也给出了解决方案:

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。
/lmj623565791/rss/https://developer.钱柜娱乐开户.com/about/versions/nougat/钱柜娱乐开户-7.0-changes.html#accessibility

那么下面就看看如何通过FileProvider解决此问题吧。

三、使用FileProvider兼容拍照

其实对于如何使用FileProvider,其实在FileProvider的API页面也有详细的步骤,有兴趣的可以看下。

/lmj623565791/rss/https://developer.钱柜娱乐开户.com/reference/钱柜娱乐开户/support/v4/content/FileProvider.html

FileProvider实际上是ContentProvider的一个子类,它的作用也比较明显了,file:///Uri不给用,那么换个Uri为content://来替代。

下面我们看下整体的实现步骤,并考虑为什么需要怎么做?

(1)声明provider

<provider
    钱柜娱乐开户:name="钱柜娱乐开户.support.v4.content.FileProvider"
    钱柜娱乐开户:authorities="com.zhy.钱柜娱乐开户7.fileprovider"
    钱柜娱乐开户:exported="false"
    钱柜娱乐开户:grantUriPermissions="true">
    <meta-data
        钱柜娱乐开户:name="钱柜娱乐开户.support.FILE_PROVIDER_PATHS"
        钱柜娱乐开户:resource="@xml/file_paths" />
</provider>

为什么要声明呢?因为FileProvider是ContentProvider子类哇~~

注意一点,他需要设置一个meta-data,里面指向一个xml文件。

(2)编写resource xml file

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户">
    <root-path name="root" path="" />
    <files-path name="files" path="" />
    <cache-path name="cache" path="" />
    <external-path name="external" path="" />
    <external-files-path name="name" path="path" />
     <external-cache-path name="name" path="path" />
</paths>

在paths节点内部支持以下几个子节点,分别为:

  • <root-path/> 代表设备的根目录new File("/");
  • <files-path/> 代表context.getFilesDir()
  • <cache-path/> 代表context.getCacheDir()
  • <external-path/> 代表Environment.getExternalStorageDirectory()
  • <external-files-path>代表context.getExternalFilesDirs()
  • <external-cache-path>代表getExternalCacheDirs()

每个节点都支持两个属性:

  • name
  • path

path即为代表目录下的子目录,比如:

<external-path
        name="external"
        path="pics" />

代表的目录即为:Environment.getExternalStorageDirectory()/pics,其他同理。

当这么声明以后,代码可以使用你所声明的当前文件夹以及其子文件夹。

本例使用的是SDCard所以这么写即可:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户">
    <external-path name="external" path="" />
</paths>

为了简单,我们直接使用SDCard根目录,所以path里面就不填写子目录了~

这里你可能会有疑问,为什么要写这么个xml文件,有啥用呀?

刚才我们说了,现在要使用content://uri替代file://uri,那么,content://的uri如何定义呢?总不能使用文件路径吧,那不是骗自己么~

所以,需要一个虚拟的路径对文件路径进行映射,所以需要编写个xml文件,通过path以及xml节点确定可访问的目录,通过name属性来映射真实的文件路径。

(3)使用FileProvider API

好了,接下来就可以通过FileProvider把我们的file转化为content://uri了~

public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            mCurrentPhotoPath = file.getAbsolutePath();

            Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.钱柜娱乐开户7.fileprovider", file);
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }

核心代码就这一行了~

FileProvider.getUriForFile(this, "com.zhy.钱柜娱乐开户7.fileprovider", file);

第二个参数就是我们配置的authorities,这个很正常了,总得映射到确定的ContentProvider吧~所以需要这个参数。

然后再看一眼我们生成的uri:

content://com.zhy.钱柜娱乐开户7.fileprovider/external/20170601-041411.png

可以看到格式为:content://authorities/定义的name属性/文件的相对路径,即name隐藏了可存储的文件夹路径。

现在拿7.0的原生手机运行就正常啦~

不过事情到此并没有结束~~

打开一个4.4的模拟器,运行上述代码,你会发现又Crash啦,抛出了:Permission Denial~

Caused by: java.lang.SecurityException: Permission Denial: opening provider 钱柜娱乐开户.support.v4.content.FileProvider from ProcessRecord{52b029b8 1670:com.钱柜娱乐开户.camera/u0a36} (pid=1670, uid=10036) that is not exported from uid 10052
at 钱柜娱乐开户.os.Parcel.readException(Parcel.java:1465)
at 钱柜娱乐开户.os.Parcel.readException(Parcel.java:1419)
at 钱柜娱乐开户.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)
at 钱柜娱乐开户.app.ActivityThread.acquireProvider(ActivityThread.java:4399)

因为低版本的系统,仅仅是把这个当成一个普通的Provider在使用,而我们没有授权,contentprovider的export设置的也是false;导致Permission Denial

那么,我们是否可以将export设置为true呢?

很遗憾是不能的。

在FileProvider的内部:

@Override
public void attachInfo(Context context, ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

确定了exported必须是false,grantUriPermissions必须是true ~~

所以唯一的办法就是授权了~

context提供了两个方法:

  • grantUriPermission(String toPackage, Uri uri,
    int modeFlags)
  • revokeUriPermission(Uri uri, int modeFlags);

可以看到grantUriPermission需要传递一个包名,就是你给哪个应用授权,但是很多时候,比如分享,我们并不知道最终用户会选择哪个app,所以我们可以这样:

List<ResolveInfo> resInfoList = context.getPackageManager()
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    context.grantUriPermission(packageName, uri, flag);
}

根据Intent查询出的所以符合的应用,都给他们授权~~

恩,你可以在不需要的时候通过revokeUriPermission移除权限~

那么增加了授权后的代码是这样的:

public void takePhotoNoCompress(View view) {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.钱柜娱乐开户7.fileprovider", file);

        List<ResolveInfo> resInfoList = getPackageManager()
                .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }

        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

这样就搞定了,不过还是挺麻烦的,如果你仅仅是对旧系统做兼容,还是建议做一下版本校验即可,也就是说不要管什么授权了,直接这样获取uri

Uri fileUri = null;
if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.zhy.钱柜娱乐开户7.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

这样会比较方便~也避免导致一些问题。当然了,完全使用uri也有一些好处,比如你可以使用私有目录去存储拍摄的照片~

文章最后会给出快速适配的方案~~不需要这么麻烦~

好像,还有什么知识点没有提到,再看一个例子吧~

四、使用FileProvider兼容安装apk

正常我们在编写安装apk的时候,是这样的:

public void installApk(View view) {
    File file = new File(Environment.getExternalStorageDirectory(), "test钱柜娱乐开户7-debug.apk");

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(Uri.fromFile(file),
            "application/vnd.钱柜娱乐开户.package-archive");
    startActivity(intent);
}

拿个7.0的原生手机跑一下,钱柜娱乐开户.os.FileUriExposedException又来了~~

钱柜娱乐开户.os.FileUriExposedException: file:///storage/emulated/0/test钱柜娱乐开户7-debug.apk exposed beyond app through Intent.getData()

好在有经验了,简单修改下uri的获取方式。

if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.zhy.钱柜娱乐开户7.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

再跑一次,没想到还是抛出了异常(警告,没有Crash):

java.lang.SecurityException: Permission Denial: 
opening provider 钱柜娱乐开户.support.v4.content.FileProvider 
        from ProcessRecord{18570a 27107:com.google.钱柜娱乐开户.packageinstaller/u0a26} (pid=27107, uid=10026) that is not exported from UID 10004

可以看到是权限问题,对于权限我们刚说了一种方式为grantUriPermission,这种方式当然是没问题的啦~

加上后运行即可。

其实对于权限,还提供了一种方式,即:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

我们可以在安装包之前加上上述代码,再次运行正常啦~

现在我有两个非常疑惑的问题:

  • 问题1:为什么刚才拍照的时候,钱柜娱乐开户 7的设备并没有遇到Permission Denial的问题?

恩,之所以不需要权限,主要是因为Intent的action为ACTION_IMAGE_CAPTURE,当我们startActivity后,会辗转调用Instrumentation的execStartActivity方法,在该方法内部,会调用intent.migrateExtraStreamToClipData();方法。

该方法中包含:

if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
        || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
        || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
    final Uri output;
    try {
        output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
    } catch (ClassCastException e) {
        return false;
    }
    if (output != null) {
        setClipData(ClipData.newRawUri("", output));
        addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
        return true;
    }
}

可以看到将我们的EXTRA_OUTPUT,转为了setClipData,并直接给我们添加了WRITE和READ权限。

注:该部分逻辑应该是21之后添加的。

  • 问题2:为什么刚才拍照案例的时候,钱柜娱乐开户 4.4设备遇到权限问题,不通过addFlags这种方式解决?

因为addFlags主要用于setDatasetDataAndType以及setClipData(注意:4.4时,并没有将ACTION_IMAGE_CAPTURE转为setClipData实现)这种方式。

所以addFlags方式对于ACTION_IMAGE_CAPTURE在5.0以下是无效的,所以需要使用grantUriPermission,如果是正常的通过setData分享的uri,使用addFlags是没有问题的(可以写个简单的例子测试下,两个app交互,通过content://)。

五、总结下

终于将知识点都涵盖到了~

总结下,使用content://替代file://,主要需要FileProvider的支持,而因为FileProvider是ContentProvider的子类,所以需要在钱柜娱乐开户Manifest.xml中注册;而又因为需要对真实的filepath进行映射,所以需要编写一个xml文档,用于描述可使用的文件夹目录,以及通过name去映射该文件夹目录。

对于权限,有两种方式:

  • 方式一为Intent.addFlags,该方式主要用于针对intent.setData,setDataAndType以及setClipData相关方式传递uri的。
  • 方式二为grantUriPermission来进行授权

相比来说方式二较为麻烦,因为需要指定目标应用包名,很多时候并不清楚,所以需要通过PackageManager进行查找到所有匹配的应用,全部进行授权。不过更为稳妥~

方式一较为简单,对于intent.setData,setDataAndType正常使用即可,但是对于setClipData,由于5.0前后Intent#migrateExtraStreamToClipData,代码发生变化,需要注意~

好了,看到现在是不是觉得适配7.0挺麻烦的,其实一点都不麻烦,下面给大家总结一种快速适配的方式。

六、快速完成适配

(1)新建一个module

创建一个library的module,在其钱柜娱乐开户Manifest.xml中完成FileProvider的注册,代码编写为:

<application>
    <provider
        钱柜娱乐开户:name="钱柜娱乐开户.support.v4.content.FileProvider"
        钱柜娱乐开户:authorities="${applicationId}.钱柜娱乐开户7.fileprovider"
        钱柜娱乐开户:exported="false"
        钱柜娱乐开户:grantUriPermissions="true">
        <meta-data
            钱柜娱乐开户:name="钱柜娱乐开户.support.FILE_PROVIDER_PATHS"
            钱柜娱乐开户:resource="@xml/file_paths" />
    </provider>
</application>

注意一点,钱柜娱乐开户:authorities不要写死,因为该library最终可能会让多个项目引用,而钱柜娱乐开户:authorities是不可以重复的,如果两个app中定义了相同的,则后者无法安装到手机中(authority conflict)。

同样的的编写file_paths~

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:钱柜娱乐开户="http://schemas.钱柜娱乐开户.com/apk/res/钱柜娱乐开户">
    <root-path
        name="root"
        path="" />
    <files-path
        name="files"
        path="" />

    <cache-path
        name="cache"
        path="" />

    <external-path
        name="external"
        path="" />

    <external-files-path
        name="external_file_path"
        path="" />
    <external-cache-path
        name="external_cache_path"
        path="" />

</paths>

最后再编写一个辅助类,例如:

public class FileProvider7 {

    public static Uri getUriForFile(Context context, File file) {
        Uri fileUri = null;
        if (Build.VERSION.SDK_INT >= 24) {
            fileUri = getUriForFile24(context, file);
        } else {
            fileUri = Uri.fromFile(file);
        }
        return fileUri;
    }

    public static Uri getUriForFile24(Context context, File file) {
        Uri fileUri = 钱柜娱乐开户.support.v4.content.FileProvider.getUriForFile(context,
                context.getPackageName() + ".钱柜娱乐开户7.fileprovider",
                file);
        return fileUri;
    }


    public static void setIntentDataAndType(Context context,
                                            Intent intent,
                                            String type,
                                            File file,
                                            boolean writeAble) {
        if (Build.VERSION.SDK_INT >= 24) {
            intent.setDataAndType(getUriForFile(context, file), type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (writeAble) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
        } else {
            intent.setDataAndType(Uri.fromFile(file), type);
        }
    }
}

可以根据自己的需求添加方法。

好了,这样我们的一个小库就写好了~~

(2)使用

如果哪个项目需要适配7.0,那么只需要这样引用这个库,然后只需要改动一行代码即可完成适配啦,例如:

拍照

public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider7.getUriForFile(this, file);
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

只需要改动

 Uri fileUri = FileProvider7.getUriForFile(this, file);

即可。

安装apk

同样的修改setDataAndType为:

FileProvider7.setIntentDataAndType(this,
      intent, "application/vnd.钱柜娱乐开户.package-archive", file, true);

即可。

ok,繁琐的重复性操作终于简化为一行代码啦~

源码地址:

https://github.com/hongyang钱柜娱乐开户/Fit钱柜娱乐开户7


支持我的话可以关注下我的公众号,每天都会推送新知识~

欢迎关注我的微信公众号:hongyang钱柜娱乐开户
(可以给我留言你想学习的文章,支持投稿)

参考

作者:lmj623565791 发表于 2017/06/09 09:03:46 原文链接 /lmj623565791/article/details/72859156
阅读:48182 评论:54 查看评论
]]>
Hongyang - 钱柜娱乐开户 /lmj623565791/article/details/72667669 /lmj623565791/article/details/72667669 lmj623565791 2017/05/23 23:13:53

本文已在我的公众号hongyang钱柜娱乐开户原创首发。
转载请标明出处:
/lmj623565791/article/details/72667669
本文出自张鸿洋的博客

一、概述

前面写了两篇分析了tinker的loader部分源码以及dex diff/patch算法相关解析,那么为了保证完整性,最后一篇主要写tinker-patch-gradle-plugin相关了。

(距离看的时候已经快两个月了,再不写就忘了,赶紧记录下来)

注意:

本文基于1.7.7

前两篇文章分别为:

有兴趣的可以查看~

在介绍细节之前,我们可以先考虑下:通过一个命令生成一个patch文件,这个文件可以用于下发做热修复(可修复常规代码、资源等),那么第一反应是什么呢?

正常思维,需要设置oldApk,然后我这边build生成newApk,两者需要做diff,找出不同的代码、资源,通过特定的算法将diff出来的数据打成patch文件。

ok,的确是这样的,但是上述这个过程有什么需要注意的么?

  1. 我们在新增资源的时候,可能会因为我们新增的一个资源,导致非常多的资源id发生变化,如果这样直接进行diff,可能会导致资源错乱等(id指向了错误的图片)问题。所以应当保证,当资源改变或者新增、删除资源时,早已存在的资源的id不会发生变化。
  2. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码难以保证规则一致;所以,build过程中理论上需要设置混淆的mapping文件。
  3. 当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。
  4. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。

如果大家尝试过接入tinker并使用gradle的方式生成patch相关,会发现在需要在项目的build.gradle中,添加一些配置,这些配置中,会要求我们配置oldApk路径,资源的R.txt路径,混淆mapping文件路径、还有一些比较tinker相关的比较细致的配置信息等。

不过并没有要求我们显示去处理上述几个问题(并没有让你去keep混淆规则,主dex分包规则,以及apply mapping文件),所以上述的几个实际上都是tinker的gradle plugin 帮我们做了。

所以,本文将会以这些问题为线索来带大家走一圈plugin的代码(当然实际上tinker gradle plugin所做的事情远不止上述)。

其次,tinker gradle plugin也是非常好的gradle的学习资料~

二、寻找查看代码入口

下载tinker的代码,导入后,plugin的代码都在tinker-patch-gradle-plugin中,不过当然不能抱着代码一行一行去啃了,应该有个明确的入口,有条理的去阅读这些代码。

那么这个入口是什么呢?

其实很简单,我们在打patch的时候,需要执行tinkerPatchDebug(注:本篇博客基于debug模式讲解)。

当执行完后,将会看到执行过程包含以下流程:

:app:processDebugManifest
:app:tinkerProcessDebugManifest(tinker)
:app:tinkerProcessDebugResourceId (tinker)
:app:processDebugResources
:app:tinkerProguardConfigTask(tinker)
:app:transformClassesAndResourcesWithProguard
:app:tinkerProcessDebugMultidexKeep (tinker)
:app:transformClassesWidthMultidexlistForDebug
:app:assembleDebug
:app:tinkerPatchDebug(tinker)

注:包含(tinker)的都是tinker plugin 所添加的task

可以看到部分task加入到了build的流程中,那么这些task是如何加入到build过程中的呢?

在我们接入tinker之后,build.gradle中有如下代码:

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'
    tinkerPatch {} // 各种参数
}

如果开启了tinker,会apply一个plugincom.tencent.tinker.patch

名称实际上就是properties文件的名字,该文件会对应具体的插件类。

对于gradle plugin不了解的,可以参考http://www.cnblogs.com/davenkin/p/gradle-learning-10.html,后面写会抽空单独写一篇详细讲gradle的文章。

下面看TinkerPatchPlugin,在apply方法中,里面大致有类似的代码:

// ... 省略了一堆代码
TinkerPatchSchemaTask tinkerPatchBuildTask 
        = project.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask)
tinkerPatchBuildTask.dependsOn variant.assemble

TinkerManifestTask manifestTask 
        = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

TinkerResourceIdTask applyResourceTask 
        = project.tasks.create("tinkerProcess${variantName}ResourceId", TinkerResourceIdTask)
applyResourceTask.mustRunAfter manifestTask
variantOutput.processResources.dependsOn applyResourceTask

if (proguardEnable) {
    TinkerProguardConfigTask proguardConfigTask 
            = project.tasks.create("tinkerProcess${variantName}Proguard", TinkerProguardConfigTask)
    proguardConfigTask.mustRunAfter manifestTask

    def proguardTask = getProguardTask(project, variantName)
    if (proguardTask != null) {
        proguardTask.dependsOn proguardConfigTask
    }

}
if (multiDexEnabled) {
    TinkerMultidexConfigTask multidexConfigTask 
            = project.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask)
    multidexConfigTask.mustRunAfter manifestTask

    def multidexTask = getMultiDexTask(project, variantName)
    if (multidexTask != null) {
        multidexTask.dependsOn multidexConfigTask
    }
}

可以看到它通过gradle Project API创建了5个task,通过dependsOn,mustRunAfter插入到了原本的流程中。

例如:

TinkerManifestTask manifestTask = ...
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

TinkerManifestTask必须在processManifest之后执行,processResources在manifestTask后执行。

所以流程变为:

processManifest-> manifestTask-> processResources

其他同理。

ok,大致了解了这些task是如何注入的之后,接下来就看看每个task的具体作用吧。

注:如果我们有需求在build过程中搞事,可以参考上述task编写以及依赖方式的设置。

三、每个Task的具体行为

我们按照上述的流程来看,依次为:

TinkerManifestTask
TinkerResourceIdTask
TinkerProguardConfigTask
TinkerMultidexConfigTask
TinkerPatchSchemaTask

丢个图,对应下:

四、TinkerManifestTask

#TinkerManifestTask
@TaskAction
def updateManifest() {
    // Parse the 钱柜娱乐开户Manifest.xml
    String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId

    tinkerValue = TINKER_ID_PREFIX + tinkerValue;//"tinker_id_"

    // /build/intermediates/manifests/full/debug/钱柜娱乐开户Manifest.xml
    writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)

    addApplicationToLoaderPattern()
    File manifestFile = new File(manifestPath)
    if (manifestFile.exists()) {
        FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
    }
}

这里主要做了两件事:

  • writeManifestMeta主要就是解析钱柜娱乐开户Manifest.xml,在<application>内部添加一个meta标签,value为tinkerValue。

    例如:

     <meta-data
            钱柜娱乐开户:name="TINKER_ID"
            钱柜娱乐开户:value="tinker_id_com.zhy.abc" />

这里不详细展开了,话说groovy解析XML真方便。

  • addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader中。

最后copy修改后的钱柜娱乐开户Manifest.xmlbuild/intermediates/tinker_intermediates/钱柜娱乐开户Manifest.xml

这里我们需要想一下,在文初的分析中,并没有想到需要tinkerId这个东西,那么它到底是干嘛的呢?

看一下微信提供的参数说明,就明白了:

在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。

想一下,在非强制升级的情况下,线上一般分布着各个版本的app。但是。你打patch肯定是对应某个版本,所以你要保证这个patch下发下去只影响对应的版本,不会对其他版本造成影响,所以你需要tinkerId与具体的版本相对应。

ok,下一个TinkerResourceIdTask。

五、TinkerResourceIdTask

文初提到,打patch的过程实际上要控制已有的资源id不能发生变化,这个task所做的事就是为此。

如果保证已有资源的id保持不变呢?

实际上需要public.xmlids.xml的参与,即预先在public.xml中的如下定义,在第二次打包之后可保持该资源对应的id值不变。

注:对xml文件的名称应该没有强要求。

<public type="id" name="search_button" id="0x7f0c0046" />

很多时候我们在搜索固化资源,一般都能看到通过public.xml去固化资源id,但是这里有个ids.xml是干嘛的呢?

下面这篇文章有个很好的解释~

/sbsujjbcy/article/details/52541803

首先需要生成public.xml,public.xml的生成通过aapt编译时添加-P参数生成。相关代码通过gradle插件去hook Task无缝加入该参数,有一点需要注意,通过appt生成的public.xml并不是可以直接用的,该文件中存在id类型的资源,生成patch时应用进去编译的时候会报resource is not defined,解决方法是将id类型型的资源单独记录到ids.xml文件中,相当于一个声明过程,编译的时候和public.xml一样,将ids.xml也参与编译即可。

ok,知道了public.xml和ids.xml的作用之后,需要再思考一下如何保证id不变?

首先我们在配置old apk的时候,会配置tinkerApplyResourcePath参数,该参数对应一个R.txt,里面的内容涵盖了所有old apk中资源对应的int值。

那么我们可以这么做,根据这个R.txt,把里面的数据写成public.xml不就能保证原本的资源对应的int值不变了么。

的确是这样的,不过tinker做了更多,不仅将old apk的中的资源信息写到public.xml,而且还干涉了新的资源,对新的资源按照资源id的生成规则,也分配的对应的int值,写到了public.xml,可以说该task包办了资源id的生成。

分析前的总结

好了,由于代码非常长,我决定在这个地方先用总结性的语言总结下,如果没有耐心看代码的可以直接跳过源码分析阶段:

首先将设置的old R.txt读取到内存中,转为:

  • 一个Map,key-value都代表一个具体资源信息;直接复用,不会生成新的资源信息。
  • 一个Map,key为资源类型,value为该类资源当前的最大int值;参与新的资源id的生成。

接下来遍历当前app中的资源,资源分为:

  • values文件夹下文件

对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap(key为资源类型,value为对应类型资源集合Set)中。

其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。

  • res下非values文件夹

打开自己的项目有看一眼,除了values相关还有layout,anim,color等文件夹,主要分为两类:

一类是对 文件 即为资源,例如R.layout.xxx,R.drawable.xxx等;另一类为xml文档中以@+(去除@+钱柜娱乐开户:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称)。

如果和设置的old apk中文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息。

会将所有生成的资源都存到rTypeResourceMap中,最后写文件。

这样就基本收集到了所有的需要生成资源信息的所有的资源,最后写到public.xml即可。

总结性的语言难免有一些疏漏,实际以源码分析为标准。

开始源码分析

@TaskAction
def applyResourceId() {
     // 资源mapping文件
    String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping

    // resDir /build/intermediates/res/merged/debug
    String idsXml = resDir + "/values/ids.xml";
    String publicXml = resDir + "/values/public.xml";
    FileOperation.deleteFile(idsXml);
    FileOperation.deleteFile(publicXml);

    List<String> resourceDirectoryList = new ArrayList<String>();
    // /build/intermediates/res/merged/debug
    resourceDirectoryList.add(resDir);

    project.logger.error("we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}");

    project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true;

    // 收集所有的资源,以type->type,name,id,int/int[]存储
    Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile);

    AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);

    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);
    File publicFile = new File(publicXml);
    if (publicFile.exists()) {
        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));
        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");
    }
    File idxFile = new File(idsXml);
    if (idxFile.exists()) {
        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));
        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");
    }
}

大体浏览下代码,可以看到首先检测是否设置了resource mapping文件,如果没有设置会直接跳过。并且最后的产物是public.xmlids.xml

因为生成patch时,需要保证两次打包已经存在的资源的id一致,需要public.xmlids.xml的参与。

首先清理已经存在的public.xmlids.xml,然后通过PatchUtil.readRTxt读取resourceMappingFile(参数中设置的),该文件记录的格式如下:

int anim abc_slide_in_bottom 0x7f050006
int id useLogo 0x7f0b0012
int[] styleable AppCompatImageView { 0x01010119, 0x7f010027 }
int styleable AppCompatImageView_钱柜娱乐开户_src 0
int styleable AppCompatImageView_srcCompat 1

大概有两类,一类是int型各种资源;一类是int[]数组,代表styleable,其后面紧跟着它的item(熟悉自定义View的一定不陌生)。

PatchUtil.readRTxt的代码就不贴了,简单描述下:

首先正则按行匹配,每行分为四部分,即idType,rType,name,idValue(四个属性为RDotTxtEntry的成员变量)。

  • idType有两种INTINT_ARRAY
  • rType包含各种资源:

ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION,
ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW,
STRING, STYLE, STYLEABLE, TRANSITION, XML

http://developer.钱柜娱乐开户.com/reference/钱柜娱乐开户/R.html

name和value就是普通的键值对了。

这里并没有对styleable做特殊处理。

最后按rType分类,存在一个Map中,即key为rType,value为一个RDotTxtEntry类型的Set集合。

回顾下剩下的代码:

//...省略前半部分
     AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);
    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);
    File publicFile = new File(publicXml);
    if (publicFile.exists()) {
        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));
        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");
    }
    File idxFile = new File(idsXml);
    if (idxFile.exists()) {
        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));
        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");
    }

那么到了AaptUtil.collectResource方法,传入了resDir目录和我们刚才收集了资源信息的Map,返回了一个AaptResourceCollector对象,看名称是对aapt相关的资源的收集:

看代码:

public static AaptResourceCollector collectResource(List<String> resourceDirectoryList,
                                                    Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {
    AaptResourceCollector resourceCollector = new AaptResourceCollector(rTypeResourceMap);
    List<com.tencent.tinker.build.aapt.RDotTxtEntry> references = new ArrayList<com.tencent.tinker.build.aapt.RDotTxtEntry>();
    for (String resourceDirectory : resourceDirectoryList) {
        try {
            collectResources(resourceDirectory, resourceCollector);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    for (String resourceDirectory : resourceDirectoryList) {
        try {
            processXmlFilesForIds(resourceDirectory, references, resourceCollector);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    return resourceCollector;
}

首先初始化了一个AaptResourceCollector对象,看其构造方法:

public AaptResourceCollector(Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {
    this();
    if (rTypeResourceMap != null) {
        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = rTypeResourceMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();
            RType rType = entry.getKey();
            Set<RDotTxtEntry> set = entry.getValue();

            for (RDotTxtEntry rDotTxtEntry : set) {
                originalResourceMap.put(rDotTxtEntry, rDotTxtEntry);

                ResourceIdEnumerator resourceIdEnumerator = null;
                    // ARRAY主要是styleable
                if (!rDotTxtEntry.idType.equals(IdType.INT_ARRAY)) {
                        // 获得resourceId
                    int resourceId = Integer.decode(rDotTxtEntry.idValue.trim()).intValue();
                    // 获得typeId
                    int typeId = ((resourceId & 0x00FF0000) / 0x00010000);


                    if (typeId >= currentTypeId) {
                        currentTypeId = typeId + 1;
                    }

                        // type -> id的映射
                    if (this.rTypeEnumeratorMap.containsKey(rType)) {
                        resourceIdEnumerator = this.rTypeEnumeratorMap.get(rType);
                        if (resourceIdEnumerator.currentId < resourceId) {
                            resourceIdEnumerator.currentId = resourceId;
                        }
                    } else {
                        resourceIdEnumerator = new ResourceIdEnumerator();
                        resourceIdEnumerator.currentId = resourceId;
                        this.rTypeEnumeratorMap.put(rType, resourceIdEnumerator);
                    }
                }
            }
        }
    }
}

对rTypeResourceMap根据rType进行遍历,读取每个rType对应的Set集合;然后遍历每个rDotTxtEntry:

  1. 加入到originalResourceMap,key和value都是rDotTxtEntry对象
  2. 如果是int型资源,首先读取其typeId,并持续更新currentTypeId(保证其为遍历完成后的最大值+1)
  3. 初始化rTypeEnumeratorMap,key为rType,value为ResourceIdEnumerator,且ResourceIdEnumerator中的currentId保存着目前同类资源的最大的resouceId,也就是说rTypeEnumeratorMap中存储了各个rType对应的最大的资源Id。

结束完成构造方法,执行了

  1. 遍历了resourceDirectoryList,目前其中只有一个resDir,然后执行了collectResources方法;
  2. 遍历了resourceDirectoryList,执行了processXmlFilesForIds

分别读代码了:

collectResources

private static void collectResources(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception {
    File resourceDirectoryFile = new File(resourceDirectory);
    File[] fileArray = resourceDirectoryFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isDirectory()) {
                String directoryName = file.getName();
                if (directoryName.startsWith("values")) {
                    if (!isAValuesDirectory(directoryName)) {
                        throw new AaptUtilException("'" + directoryName + "' is not a valid values directory.");
                    }
                    processValues(file.getAbsolutePath(), resourceCollector);
                } else {
                    processFileNamesInDirectory(file.getAbsolutePath(), resourceCollector);
                }
            }
        }
    }
}

遍历我们的resDir中的所有文件夹

  • 如果是values相关文件夹,执行processValues
  • 非values相关文件夹则执行processFileNamesInDirectory

processValues处理values相关文件,会遍历每一个合法的values相关文件夹下的文件,执行processValuesFile(file.getAbsolutePath(), resourceCollector);

public static void processValuesFile(String valuesFullFilename,
                                     AaptResourceCollector resourceCollector) throws Exception {
    Document document = JavaXmlUtil.parse(valuesFullFilename);
    String directoryName = new File(valuesFullFilename).getParentFile().getName();
    Element root = document.getDocumentElement();

    for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
        if (node.getNodeType() != Node.ELEMENT_NODE) {
            continue;
        }

        String resourceType = node.getNodeName();
        if (resourceType.equals(ITEM_TAG)) {
            resourceType = node.getAttributes().getNamedItem("type").getNodeValue();
            if (resourceType.equals("id")) {
                resourceCollector.addIgnoreId(node.getAttributes().getNamedItem("name").getNodeValue());
            }
        }

        if (IGNORED_TAGS.contains(resourceType)) {
            continue;
        }

        if (!RESOURCE_TYPES.containsKey(resourceType)) {
            throw new AaptUtilException("Invalid resource type '<" + resourceType + ">' in '" + valuesFullFilename + "'.");
        }

        RType rType = RESOURCE_TYPES.get(resourceType);
        String resourceValue = null;
        switch (rType) {
            case STRING:
            case COLOR:
            case DIMEN:
            case DRAWABLE:
            case BOOL:
            case INTEGER:
                resourceValue = node.getTextContent().trim();
                break;
            case ARRAY://has sub item
            case PLURALS://has sub item
            case STYLE://has sub item
            case STYLEABLE://has sub item
                resourceValue = subNodeToString(node);
                break;
            case FRACTION://no sub item
                resourceValue = nodeToString(node, true);
                break;
            case ATTR://no sub item
                resourceValue = nodeToString(node, true);
                break;
        }
        try {
            addToResourceCollector(resourceCollector,
                    new ResourceDirectory(directoryName, valuesFullFilename),
                    node, rType, resourceValue);
        } catch (Exception e) {
            throw new AaptUtilException(e.getMessage() + ",Process file error:" + valuesFullFilename, e);
        }
    }
}

values下相关的文件基本都是xml咯,所以遍历xml文件,遍历其内部的节点,(values的xml文件其内部一般为item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction等),每种类型的节点对应一个rType,根据不同类型的节点也会去获取节点的值,确定一个都会执行:

addToResourceCollector(resourceCollector,
    new ResourceDirectory(directoryName, valuesFullFilename),
    node, rType, resourceValue);

注:除此以外,这里在ignoreIdSet记录了声明的id资源,这些id是已经声明过的,所以最终在编写ids.xml时,可以过滤掉这些id。

下面继续看:addToResourceCollector

源码如下:

private static void addToResourceCollector(AaptResourceCollector resourceCollector,
                                           ResourceDirectory resourceDirectory,
                                           Node node, RType rType, String resourceValue) {
    String resourceName = sanitizeName(rType, resourceCollector, extractNameAttribute(node));

    if (rType.equals(RType.STYLEABLE)) {

        int count = 0;
        for (Node attrNode = node.getFirstChild(); attrNode != null; attrNode = attrNode.getNextSibling()) {
            if (attrNode.getNodeType() != Node.ELEMENT_NODE || !attrNode.getNodeName().equals("attr")) {
                continue;
            }
            String rawAttrName = extractNameAttribute(attrNode);
            String attrName = sanitizeName(rType, resourceCollector, rawAttrName);

            if (!rawAttrName.startsWith("钱柜娱乐开户:")) {
                resourceCollector.addIntResourceIfNotPresent(RType.ATTR, attrName);
            }
        }
    } else {
        resourceCollector.addIntResourceIfNotPresent(rType, resourceName);
    }
}

如果不是styleable的资源,则直接获取resourceName,然后调用resourceCollector.addIntResourceIfNotPresent(rType, resourceName)。

如果是styleable类型的资源,则会遍历找到其内部的attr节点,找出非钱柜娱乐开户:开头的(因为钱柜娱乐开户:开头的attr的id不需要我们去确定),设置rType为ATTR,value为attr属性的name,调用addIntResourceIfNotPresent。

public void addIntResourceIfNotPresent(RType rType, String name) { //, ResourceDirectory resourceDirectory) {
    if (!rTypeEnumeratorMap.containsKey(rType)) {
        if (rType.equals(RType.ATTR)) {
            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(1));
        } else {
            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(currentTypeId++));
        }
    }

    RDotTxtEntry entry = new FakeRDotTxtEntry(IdType.INT, rType, name);
    Set<RDotTxtEntry> resourceSet = null;
    if (this.rTypeResourceMap.containsKey(rType)) {
        resourceSet = this.rTypeResourceMap.get(rType);
    } else {
        resourceSet = new HashSet<RDotTxtEntry>();
        this.rTypeResourceMap.put(rType, resourceSet);
    }
    if (!resourceSet.contains(entry)) {
        String idValue = String.format("0x%08x", rTypeEnumeratorMap.get(rType).next());
        addResource(rType, IdType.INT, name, idValue); //, resourceDirectory);
    }
}

首先构建一个entry,然后判断当前的rTypeResourceMap中是否存在该资源实体,如果存在,则什么都不用做。

如果不存在,则需要构建一个entry,那么主要是id的构建。

关于id的构建:

还记得rTypeEnumeratorMap么,其内部包含了我们设置的”res mapping”文件,存储了每一类资源(rType)的资源的最大resourceId值。

那么首先判断就是是否已经有这种类型了,如果有的话,获取出该类型当前最大的resourceId,然后+1,最为传入资源的resourceId.

如果不存在当前这种类型,那么如果类型为ATTR则固定type为1;否则的话,新增一个typeId,为当前最大的type+1(currentTypeId中也是记录了目前最大的type值),有了类型就可以通过ResourceIdEnumerator.next()来获取id。

经过上述就可以构造出一个idValue了。

最后调用:

addResource(rType, IdType.INT, name, idValue);

查看代码:

public void addResource(RType rType, IdType idType, String name, String idValue) {
    Set<RDotTxtEntry> resourceSet = null;
    if (this.rTypeResourceMap.containsKey(rType)) {
        resourceSet = this.rTypeResourceMap.get(rType);
    } else {
        resourceSet = new HashSet<RDotTxtEntry>();
        this.rTypeResourceMap.put(rType, resourceSet);
    }
    RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue);

    if (!resourceSet.contains(rDotTxtEntry)) {
        if (this.originalResourceMap.containsKey(rDotTxtEntry)) {
            this.rTypeEnumeratorMap.get(rType).previous();
            rDotTxtEntry = this.originalResourceMap.get(rDotTxtEntry);
        } 
        resourceSet.add(rDotTxtEntry);
    }

}

大体意思就是如果该资源不存在就添加到rTypeResourceMap。

首先构建出该资源实体,判断该类型对应的资源集合是否包含该资源实体(这里contains只比对name和type),如果不包含,判断是否在originalResourceMap中,如果存在(这里做了一个previous操作,其实与上面的代码的next操作对应,主要是针对资源存在我们的res map中这种情况)则取出该资源实体,最终将该资源实体加入到rTypeResourceMap中。

ok,到这里需要小节一下,我们刚才对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap中(如果和设置的”res map”文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息)。

其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。

处理完成values相关文件夹之后,还需要处理一些res下的其他文件,比如layout、layout、anim等文件夹,该类资源也需要在R中生成对应的id值,这类值也需要固化。

processFileNamesInDirectory

public static void processFileNamesInDirectory(String resourceDirectory,
                                               AaptResourceCollector resourceCollector) throws IOException {
    File resourceDirectoryFile = new File(resourceDirectory);
    String directoryName = resourceDirectoryFile.getName();
    int dashIndex = directoryName.indexOf('-');
    if (dashIndex != -1) {
        directoryName = directoryName.substring(0, dashIndex);
    }

    if (!RESOURCE_TYPES.containsKey(directoryName)) {
        throw new AaptUtilException(resourceDirectoryFile.getAbsolutePath() + " is not a valid resource sub-directory.");
    }
    File[] fileArray = resourceDirectoryFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isHidden()) {
                continue;
            }
            String filename = file.getName();
            int dotIndex = filename.indexOf('.');
            String resourceName = dotIndex != -1 ? filename.substring(0, dotIndex) : filename;

            RType rType = RESOURCE_TYPES.get(directoryName);
            resourceCollector.addIntResourceIfNotPresent(rType, resourceName);

            System.out.println("rType = " + rType + " , resName = " + resourceName);

            ResourceDirectory resourceDirectoryBean = new ResourceDirectory(file.getParentFile().getName(), file.getAbsolutePath());
            resourceCollector.addRTypeResourceName(rType, resourceName, null, resourceDirectoryBean);
        }
    }
}

遍历res下所有文件夹,根据文件夹名称确定其对应的资源类型(例如:drawable-xhpi,则认为其内部的文件类型为drawable类型),然后遍历该文件夹下所有的文件,最终以文件名为资源的name,文件夹确定资源的type,最终调用:

resourceCollector
.addIntResourceIfNotPresent(rType, resourceName);

processXmlFilesForIds

public static void processXmlFilesForIds(String resourceDirectory,
                                         List<RDotTxtEntry> references, AaptResourceCollector resourceCollector) throws Exception {
    List<String> xmlFullFilenameList = FileUtil
            .findMatchFile(resourceDirectory, Constant.Symbol.DOT + Constant.File.XML);
    if (xmlFullFilenameList != null) {
        for (String xmlFullFilename : xmlFullFilenameList) {
            File xmlFile = new File(xmlFullFilename);

            String parentFullFilename = xmlFile.getParent();
            File parentFile = new File(parentFullFilename);
            if (isAValuesDirectory(parentFile.getName()) || parentFile.getName().startsWith("raw")) {
                // Ignore files under values* directories and raw*.
                continue;
            }
            processXmlFile(xmlFullFilename, references, resourceCollector);
        }
    }
}

遍历除了raw*以及values*相关文件夹下的xml文件,执行processXmlFile。

public static void processXmlFile(String xmlFullFilename, List<RDotTxtEntry> references, AaptResourceCollector resourceCollector)
        throws IOException, XPathExpressionException {
    Document document = JavaXmlUtil.parse(xmlFullFilename);
    NodeList nodesWithIds = (NodeList) 钱柜娱乐开户_ID_DEFINITION.evaluate(document, XPathConstants.NODESET);
    for (int i = 0; i < nodesWithIds.getLength(); i++) {
        String resourceName = nodesWithIds.item(i).getNodeValue();


        if (!resourceName.startsWith(ID_DEFINITION_PREFIX)) {
            throw new AaptUtilException("Invalid definition of a resource: '" + resourceName + "'");
        }

        resourceCollector.addIntResourceIfNotPresent(RType.ID, resourceName.substring(ID_DEFINITION_PREFIX.length()));
    }

    // 省略了无关代码
}

主要找xml文档中以@+(去除@+钱柜娱乐开户:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称),最终调用:

resourceCollector
    .addIntResourceIfNotPresent(RType.ID, 
        resourceName.substring(ID_DEFINITION_PREFIX.length()));

上述就完成了所有的资源的收集,那么剩下的就是写文件了:


public static void generatePublicResourceXml(AaptResourceCollector aaptResourceCollector,
                                             String outputIdsXmlFullFilename,
                                             String outputPublicXmlFullFilename) {
    if (aaptResourceCollector == null) {
        return;
    }
    FileUtil.createFile(outputIdsXmlFullFilename);
    FileUtil.createFile(outputPublicXmlFullFilename);

    PrintWriter idsWriter = null;
    PrintWriter publicWriter = null;
    try {
        FileUtil.createFile(outputIdsXmlFullFilename);
        FileUtil.createFile(outputPublicXmlFullFilename);
        idsWriter = new PrintWriter(new File(outputIdsXmlFullFilename), "UTF-8");

        publicWriter = new PrintWriter(new File(outputPublicXmlFullFilename), "UTF-8");
        idsWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        publicWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        idsWriter.println("<resources>");
        publicWriter.println("<resources>");
        Map<RType, Set<RDotTxtEntry>> map = aaptResourceCollector.getRTypeResourceMap();
        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();
            RType rType = entry.getKey();
            if (!rType.equals(RType.STYLEABLE)) {
                Set<RDotTxtEntry> set = entry.getValue();
                for (RDotTxtEntry rDotTxtEntry : set) {
                    String rawName = aaptResourceCollector.getRawName(rType, rDotTxtEntry.name);
                    if (StringUtil.isBlank(rawName)) {
                        rawName = rDotTxtEntry.name;
                    }
                    publicWriter.println("<public type=\"" + rType + "\" name=\"" + rawName + "\" id=\"" + rDotTxtEntry.idValue.trim() + "\" />");          
                }
                Set<String> ignoreIdSet = aaptResourceCollector.getIgnoreIdSet();
                for (RDotTxtEntry rDotTxtEntry : set) {
                    if (rType.equals(RType.ID) && !ignoreIdSet.contains(rDotTxtEntry.name)) {
                        idsWriter.println("<item type=\"" + rType + "\" name=\"" + rDotTxtEntry.name + "\"/>");
                    } 
                }
            }
            idsWriter.flush();
            publicWriter.flush();
        }
        idsWriter.println("</resources>");
        publicWriter.println("</resources>");
    } catch (Exception e) {
        throw new PatchUtilException(e);
    } finally {
        if (idsWriter != null) {
            idsWriter.flush();
            idsWriter.close();
        }
        if (publicWriter != null) {
            publicWriter.flush();
            publicWriter.close();
        }
    }
}

主要就是遍历rTypeResourceMap,然后每个资源实体对应一条public标签记录写到public.xml中。

此外,如果发现该元素节点的type为Id,并且不在ignoreSet中,会写到ids.xml这个文件中。(这里有个ignoreSet,这里ignoreSet中记录了values下所有的<item type=id的资源,是直接在项目中已经声明过的,所以去除)。

六、TinkerProguardConfigTask

还记得文初说:

  1. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码差别应该非常巨大;所以,build过程中理论上需要设置混淆的mapping文件。
  2. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。

这个task的作用很明显了。有时候为了确保一些类在main dex中,简单的做法也会对其在混淆配置中进行keep(避免由于混淆造成类名更改,而使main dex的keep失效)。

如果开启了proguard会执行该task。

这个就是主要去设置混淆的mapping文件,和keep一些必要的类了。

@TaskAction
def updateTinkerProguardConfig() {
    def file = project.file(PROGUARD_CONFIG_PATH)
    project.logger.error("try update tinker proguard file with ${file}")

    // Create the directory if it doesnt exist already
    file.getParentFile().mkdirs()

    // Write our recommended proguard settings to this file
    FileWriter fr = new FileWriter(file.path)

    String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

    //write applymapping
    if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
        project.logger.error("try add applymapping ${applyMappingFile} to build the package")
        fr.write("-applymapping " + applyMappingFile)
        fr.write("\n")
    } else {
        project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")
    }

    fr.write(PROGUARD_CONFIG_SETTINGS)

    fr.write("#your dex.loader patterns here\n")
    //they will removed when apply
    Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
    for (String pattern : loader) {
        if (pattern.endsWith("*") && !pattern.endsWith("**")) {
            pattern += "*"
        }
        fr.write("-keep class " + pattern)
        fr.write("\n")
    }
    fr.close()
    // Add this proguard settings file to the list
    applicationVariant.getBuildType().buildType.proguardFiles(file)
    def files = applicationVariant.getBuildType().buildType.getProguardFiles()

    project.logger.error("now proguard files is ${files}")
}

读取我们设置的mappingFile,设置

-applymapping applyMappingFile

然后设置一些默认需要keep的规则:

PROGUARD_CONFIG_SETTINGS =
"-keepattributes *Annotation* \n" +
"-dontwarn com.tencent.tinker.anno.AnnotationProcessor \n" +
"-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *\n" +
"-keep public class * extends 钱柜娱乐开户.app.Application {\n" +
"    *;\n" +
"}\n" +
"\n" +
"-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +
"    *;\n" +
"}\n" +
"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +
"    *;\n" +
"}\n" +
"\n" +
"-keep public class com.tencent.tinker.loader.TinkerLoader {\n" +
"    *;\n" +
"}\n" +
"-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n" +
"    *;\n" +
"}\n" +
"-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {\n" +
"    *;\n" +
"}\n" +
"\n"

最后是keep住我们的application、com.tencent.tinker.loader.**以及我们设置的相关类。

TinkerManifestTask中:addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader

七、TinkerMultidexConfigTask

对应文初:

当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。

如果multiDexEnabled开启。

主要是让相关类必须在main dex。

"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +
    "    *;\n" +
    "}\n" +
    "\n" +
    "-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n" +
    "    *;\n" +
    "}\n" +
    "\n" +
    "-keep public class * extends 钱柜娱乐开户.app.Application {\n" +
    "    *;\n" +
    "}\n"
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
    for (String pattern : loader) {
        if (pattern.endsWith("*")) {
            if (!pattern.endsWith("**")) {
                pattern += "*"
            }
        }
        lines.append("-keep class " + pattern + " {\n" +
                "    *;\n" +
                "}\n")
                .append("\n")
    }

相关类都在loader这个集合中,在TinkerManifestTask中设置的。

八、TinkerPatchSchemaTask

主要执行Runner.tinkerPatch

protected void tinkerPatch() {
    try {
        //gen patch
        ApkDecoder decoder = new ApkDecoder(config);
        decoder.onAllPatchesStart();
        decoder.patch(config.mOldApkFile, config.mNewApkFile);
        decoder.onAllPatchesEnd();

        //gen meta file and version file
        PatchInfo info = new PatchInfo(config);
        info.gen();

        //build patch
        PatchBuilder builder = new PatchBuilder(config);
        builder.buildPatch();

    } catch (Throwable e) {
        e.printStackTrace();
        goToError();
    }
}

主要分为以下环节:

  • 生成patch
  • 生成meta-file和version-file,这里主要就是在assets目录下写一些键值对。(包含tinkerId以及配置中configField相关信息)
  • build patch

(1)生成pacth

顾名思义就是两个apk比较去生成各类patch文件,那么从一个apk的组成来看,大致可以分为:

  • dex文件比对的patch文件
  • res文件比对的patch res文件
  • so文件比对生成的so patch文件

看下代码:

public boolean patch(File oldFile, File newFile) throws Exception {
    //check manifest change first
    manifestDecoder.patch(oldFile, newFile);

    unzipApkFiles(oldFile, newFile);

    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(),
            mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

代码内部包含四个Decoder:

  • manifestDecoder
  • dexPatchDecoder
  • soPatchDecoder
  • resPatchDecoder

刚才提到需要对dex、so、res文件做diff,但是为啥会有个manifestDecoder。目前tinker并不支持四大组件,也就是说manifest文件中是不允许出现新增组件的。

所以,manifestDecoder的作用实际上是用于检查的:

  1. minSdkVersion<14时仅允许dexMode使用jar模式(TODO:raw模式的区别是什么?)
  2. 会解析manifest文件,读取出组大组件进行对比,不允许出现新增的任何组件。

代码就不贴了非常好理解,关于manifest的解析是基于该库封装的:

https://github.com/clearthesky/apk-parser

然后就是解压两个apk文件了,old apk(我们设置的),old apk 生成的。

解压的目录为:

  • old apk: build/intermediates/outputs/old apk名称/
  • new apk: build/intermediates/outputs/app-debug/

解压完成后,就是单个文件对比了:

对比的思路是,以newApk解压目录下所有的文件为基准,去oldApk中找同名的文件,那么会有以下几个情况:

  1. 在oldApkDir中没有找到,那么说明该文件是新增的
  2. 在oldApkDir中找到了,那么比对md5,如果不同,则认为改变了(则需要根据情况做diff)

有了大致的了解后,可以看代码:

Files.walkFileTree(
    mNewApkDir.toPath(), 
    new ApkFilesVisitor(
        config, 
        mNewApkDir.toPath(),
        mOldApkDir.toPath(), 
        dexPatchDecoder, 
        soPatchDecoder, 
        resPatchDecoder));

Files.walkFileTree会以mNewApkDir.toPath()为基准,遍历其内部所有的文件,ApkFilesVisitor中可以对每个遍历的文件进行操作。

重点看ApkFilesVisitor是如何操作每个文件的:

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

    Path relativePath = newApkPath.relativize(file);
    // 在oldApkDir中找到该文件
    Path oldPath = oldApkPath.resolve(relativePath);

    File oldFile = null;
    //is a new file?!
    if (oldPath.toFile().exists()) {
        oldFile = oldPath.toFile();
    }

    String patternKey = relativePath.toString()