android投屏技术🔥🔥:发现设备源码分析

1,620 阅读7分钟

cover

前言

上篇文章是关于发现设备代码实现过程,本来这两篇文章是一起的,写着写着发现实在是太长了,我担心会看着会消化不良,所以分开了。

关于 android 投屏技术系列:
一、android设备投屏技术🔥:协议&概念
这章主要讲一些基本概念, 那些 DLNA 类库都是基于这些概念来做的,了解这些概念能帮助你理清思路,同时可以提升开发效率,遇到问题也能有个解决问题的清晰思路。

二、android投屏技术🔥🔥:发现设备代码实现 & android投屏技术🔥🔥:发现设备源码分析
这部分是通过Cling DLNA类库来实现发现设备的。
内容包括:

  1. 抽出发现设备所需接口
  2. 发现设备步骤的实现
  3. 原理的分析

三、android投屏技术🔥🔥🔥:控制设备&源码分析(未完结,敬请期待)这部分 因为还未上代码,所以先预留着。(保证一周内上完代码)

源码分析阶段


什么源码?
打开ide看源码

我们先从入口开始:

upnpService.getControlPoint().search();

可见是 控制点执行的 search 方法。而 ControlPoint 的实现类是 ControlPointImpl:ControlPointImpl.search() 如下:

public void search() {
    search(new STAllHeader(), MXHeader.DEFAULT_VALUE);
}

STAllHeader 是什么玩意?进去看看

public class STAllHeader extends UpnpHeader<NotificationSubtype> {

    public STAllHeader() {
        setValue(NotificationSubtype.ALL);
    }

    public void setString(String s) throws InvalidHeaderException {
        if (!s.equals(NotificationSubtype.ALL.getHeaderString())) {
            throw new InvalidHeaderException("Invalid ST header value (not "+NotificationSubtype.ALL+"): " + s);
        }
    }

    public String getString() {
        return getValue().getHeaderString();
    }
}

setValue(NotificationSubtype.ALL) ???

public enum NotificationSubtype {

    ALIVE("ssdp:alive"),
    UPDATE("ssdp:update"),
    BYEBYE("ssdp:byebye"),
    ALL("ssdp:all"),
    DISCOVER("ssdp:discover"),
    PROPCHANGE("upnp:propchange");

    private String headerString;

    NotificationSubtype(String headerString) {
        this.headerString = headerString;
    }

    public String getHeaderString() {
        return headerString;
    }
}

NotificationSubtype.ALL = "ssdp:all"
是否记得 ssdp ? 这个就是发现设备的协议, ":" 后面就是一个筛选。
好了,我们继续返回到 search 中。
ControlPointImpl.search() 实际调用的是

public void search(UpnpHeader searchType, int mxSeconds) {
        log.fine("Sending asynchronous search for: " + searchType.getString());
        getConfiguration().getAsyncProtocolExecutor().execute(
                getProtocolFactory().createSendingSearch(searchType, mxSeconds)
        );
    }

下面解释一下:

getConfiguration() 返回的对象是 UpnpServiceConfiguration
getAsyncProtocolExecutor() 返回的对象是一个执行者 Executor
getProtocolFactory() 返回的对象是 ProtocolFactory
看起来最后执行的是 ProtocolFactory.createSendingSearch() 方法进行的设备发现。

这段代码仍然有很多疑惑的地方:

  1. UpnpServiceConfiguration 是什么? 有什么作用?
  2. UpnpServiceConfiguration 包含了一个 Executor 它如何工作的?
  3. ProtocolFactory 协议工厂? 它跟协议有什么关系吗?
  4. 这些乱七八糟的怎么连接起来的?

我们带着这些问题来解析源码。


先补充能量~

首先 UpnpServiceConfiguration 是不是有点面熟?其实在前面AndroidUpnpServiceImpl 中就有一个方法:

public UpnpServiceConfiguration getConfiguration();

这个 UpnpServiceConfiguration 在 AndroidUpnpServiceImpl 中实际上是 AndroidUpnpServiceConfiguration。在 AndroidUpnpServiceImpl onCreate 时构造的。其实这个东西,它是用于配置环境的,比如 AndroidUpnpServiceConfiguration 它就用于配置 android 环境,比如一些网络、xml解析、全局使用的一些方法之类的。所以想想 从这里面获取执行者(这个执行者是:ClingExecutor)也比较正常了。那么如果我们有什么特殊的需要,也可以自己定义一个配置,然后增加一些自己需要的方法等。

上面提到了 ClingExecutor 它有什么用处?

我们看一下 ClingExecutor 源码:

public static class ClingExecutor extends ThreadPoolExecutor {

        public ClingExecutor() {
            this(new ClingThreadFactory(),
                 new ThreadPoolExecutor.DiscardPolicy() {
                     // The pool is unbounded but rejections will happen during shutdown
                     @Override
                     public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
                         // Log and discard
                         log.info("Thread pool rejected execution of " + runnable.getClass());
                         super.rejectedExecution(runnable, threadPoolExecutor);
                     }
                 }
            );
        }

        public ClingExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedHandler) {
            // This is the same as Executors.newCachedThreadPool
            super(0,
                  Integer.MAX_VALUE,
                  60L,
                  TimeUnit.SECONDS,
                  new SynchronousQueue<Runnable>(),
                  threadFactory,
                  rejectedHandler
            );
        }

        @Override
        protected void afterExecute(Runnable runnable, Throwable throwable) {
            super.afterExecute(runnable, throwable);
            if (throwable != null) {
                Throwable cause = Exceptions.unwrap(throwable);
                if (cause instanceof InterruptedException) {
                    // Ignore this, might happen when we shutdownNow() the executor. We can't
                    // log at this point as the logging system might be stopped already (e.g.
                    // if it's a CDI component).
                    return;
                }
                // Log only
                log.warning("Thread terminated " + runnable + " abruptly with exception: " + throwable);
                log.warning("Root cause: " + cause);
            }
        }
    }

通过源码我们可以知道两点:

  1. ClingExecutor 继承 ThreadPoolExecutor(线程池) 说明它就是执行线程池相关配置等工作的。
  2. 里面提到了 ClingThreadFactory,说明线程是在这里创建的。

ClingExecutor 在 AndroidUpnpServiceConfiguration 父类 DefaultUpnpServiceConfiguration 构造中 构造的:

   protected ExecutorService getDefaultExecutorService() {
        return defaultExecutorService;
    }

    protected ExecutorService createDefaultExecutorService() {
        return new ClingExecutor();
    }

在 发现设备 中,是通过获取 AndroidUpnpServiceConfiguration 的执行者,返回的就是这个 ClingExecutor

    public Executor getAsyncProtocolExecutor() {
        return getDefaultExecutorService();
    }

在 控制设备 中,也是通过获取 AndroidUpnpServiceConfiguration 中的执行者,它返回的还是这个 ClingExecutor

public ExecutorService getSyncProtocolExecutorService() {
        return getDefaultExecutorService();
    }

所以在 AndroidUpnpServiceConfiguration 中所有的返回执行者都是返回的它。而且控制点的命令都是通过这个执行者来完成的,说明它担任了 Cling 中很重要的角色。

执行过程的话,简单来说就是发一个指令(这个指令要么是 发现设备协议 要么是 控制设备协议 的指令),Executor 执行的 就是一个 Runnable 了。在 发现设备中 这个 Runnable 的实现类是 SendingSearch;控制设备 中实现类是 ActionCallback。(这篇文章 主要分析一下 发现设备,控制设备 下篇文章来看)

下面我们看一下 SendingSearch 的精简版源码:

public abstract class SendingAsync implements Runnable {
...
}

public class SendingSearch extends SendingAsync {
...

    // 发现设备 最后执行方法, 是它 是它 就是它。。
    protected void execute() throws RouterException {

        log.fine("Executing search for target: " + searchTarget.getString() + " with MX seconds: " + getMxSeconds());

        OutgoingSearchRequest msg = new OutgoingSearchRequest(searchTarget, getMxSeconds());
        prepareOutgoingSearchRequest(msg);

        for (int i = 0; i < getBulkRepeat(); i++) {
            try {

                getUpnpService().getRouter().send(msg);

                // UDA 1.0 is silent about this but UDA 1.1 recommends "a few hundred milliseconds"
                log.finer("Sleeping " + getBulkIntervalMilliseconds() + " milliseconds");
                Thread.sleep(getBulkIntervalMilliseconds());

            } catch (InterruptedException ex) {
                // Interruption means we stop sending search messages, e.g. on shutdown of thread pool
                break;
            }
        }
    }

...
}

我们一起分析一下,你要听 简单版 还是 复杂版?
简单版:
重要代码:getUpnpService().getRouter().send(msg);
翻译出来就是 向路由 发消息;
这个消息 msg 是 OutgoingSearchRequest 的实例,OutgoingSearchRequest 里面就封装了 发现设备 的请求内容。

复杂版:
%……¥……%%&%@#$@@%#%$%?^%&^&((
(太复杂了,系统无法翻译)

不调你口味了。。 还是详细看一下这里面的疑点:

  1. 这个路由是什么鬼?
  2. SendingSearch 在哪定义的?

首先 getRouter 是不是在哪看过? 是的 在上篇文章,AndroidUpnpServiceImpl 里面看过。在 AndroidUpnpServiceImpl 里,getRouter() 是 AndroidRouter 对象。

public class AndroidRouter extends RouterImpl {
...
}

getUpnpService().getRouter().send(msg); send 方法实际在 RouterImpl 中。看一下:

    /**
     * Sends the UDP datagram on all bound {@link org.fourthline.cling.transport.spi.DatagramIO}s.
     *
     * @param msg The UDP datagram message to send.
     */
    public void send(OutgoingDatagramMessage msg) throws RouterException {
        lock(readLock);
        try {
            if (enabled) {
                for (DatagramIO datagramIO : datagramIOs.values()) {
                    datagramIO.send(msg);
                }
            } else {
                log.fine("Router disabled, not sending datagram: " + msg);
            }
        } finally {
            unlock(readLock);
        }
    }

DatagramIO 它的实现类是 DatagramIOImpl,send 方法其实就是发io流给路由了。这个路由 就是封装了一些网络相关的内容,包括网络地址、发送io流的内容等等。

回想一下发现设备流程,我们首先确保 android手机 跟 tv盒子在同一个网络下(这样 tv盒子其实向路由发送了自己的信息),然后 我们的手机设备告诉路由 我们需要什么样的设备(支持投屏),路由通过我们的需求在设备列表中筛选完之后 我们就得到了这些设备。

好了,发现设备流程还剩最后一步就结束了:向路由发完消息之后 我怎么得到设备列表的?

  1. 这些设备保存在哪里?
  2. 我们如何被通知到设备的改变?

这些设备保存在哪里?
这些设备是保存在 RegistryImpl 中:
保存的设备有两种:一个是 RemoteItems(远程设备,不是当前设备);另一个是 LocalItems(本地设备,就是当前设备)。他们就相当于列表。

我们如何被通知到设备的改变?
是否记得上篇文章提到的,监听。发现设备之后 会回调到我们定义的监听。
其实在 lan 层会截获到路由发的消息,然后会通知到我们。

总结一下:

  1. AndroidUpnpServiceConfiguration 它就用于配置 android 环境,比如一些网络、xml解析、全局使用的一些方法之类的。那么如果我们有什么特殊的需要,也可以自己定义一个配置,然后增加一些自己需要的方法等。
  2. ClingExecutor 是一个执行者,发现设备、控制设备命令都是由它来执行
  3. 发现设备实际是通过 SendingSearch 来实现的,控制设备则是 ActionCallback(下篇文章会提到)
  4. SendingSearch 其实就是向路由发消息,告诉路由 我需要什么样的设备,路由会筛选,通过我们定义的监听返回给我们。
大功告成,我终于可以开心的玩耍了

收工

下集预告:我们现在已经成功发现了 tv盒子,我们要投屏必须要控制它的 播放、暂停、停止、拖拽等操作。下集我们会一步步 来实现这些操作。

下集看点:

  1. 设备控制 的代码实现
  2. 设备控制 原理分析