当模块化遇上多渠道

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。

最近在研究Android模块化开发的一些东西, 网上大多数模块化的文章都是仅仅从一个demo的角度去看待的, 其实对于在真实项目中使用还有很多坑需要去踩, 今天就来聊聊我在模块化探索过程中遇到的众多坑中的一个-多渠道.

传统多渠道

说道多渠道, 其实大部分开发者都会在项目中使用到, 例如按照Google Play, Qihoo360… 等渠道分发, 那么我们可能这么写.

productFlavors {
  GooglePlay {}
  Qihoo360 {}
}

当我们需要根据不同渠道去编译不用的库文件的时候, 我们可能会这么写.

GooglePlayCompile (name: ‘lib_google_play‘, ext: ‘aar‘)
Qihoo360Compile (name: ‘lib_qihoo_360‘, ext: ‘aar‘)

这么写在普通的Android项目中一点毛病也没有, 那么当我们将项目模块化后呢? 这样的渠道信息还需要在每个模块中写一遍, 而且当我们增加渠道的时候还需要在每个模块中去添加渠道信息,添加渠道compile语句, 这个最终会是一个可怕的工作量. 而且, 当你的项目的build.gradle中出现大坨下面的代码的时候, 你是不是也会疯狂起来.

GooglePlayCompile (name: ‘lib_google_play‘, ext: ‘aar‘)
Qihoo360Compile (name: ‘lib_qihoo_360‘, ext: ‘aar‘)
...

GooglePlayCompile (name: ‘biz-user_google_play‘, ext: ‘aar‘)
Qihoo360Compile (name: ‘biz-user_qihoo_360‘, ext: ‘aar‘)
...

上面的类似biz-XXX的写法肯定不在少数, 因为, 当我们进行模块化开发的时候, 业务代码肯定是希望下沉的, 那样, 任何一个module都能使用. 好, 今天的重点不是讨论模块化, 关于模块化的东西我们暂且不谈.

当我看到上面的使用方式的时候, 我是不能接受的, 所以我一直在寻找合适的方式来区分渠道, 下面再来说说我在寻找这个方法过程中踩过的坑.

踩坑阶段

当我分析上面compile语句后, 我发现这些代码都是有规律的, 如果按照这个规律来讲, 这些代码都是重复的. 来看看下面两个库.

  1. lib_google_play
  2. lib_qihoo_360

其实这两个库都是对lib的封装, 不同的地方是渠道不同而已, 所以首先我想到的是要规范库文件的名称, 就像上面的lib一样, 库的名字必须是名称_渠道名的小写字母. 有了第一次突破, 下面就来思考如何简化了.

首先我想到的, 就是包装一下compile语句, 代码如下:

class Flavors {
    static String flavorType = ""

    static void onTaskEach(String task) {

        if(task.contains("GooglePlay")){
            flavorType = "GooglePlay"
        } else if (task.contains("Qihoo360")){
            flavorType = "Qihoo360"
        }
    }

    static void compile(DependencyHandler dh, String lib) {
        if (flavorType == null || flavorType == "") { return}
        dh.compile(name: "${lib}_${flavorType.toLowerCase()}", ext: ‘aar‘)
    }
}

gradle.startParameter.getTaskNames().each { task ->
    if (task.startsWith(":app")) {
        Flavors.onTaskEach(task)
    }
}

dependencies {
  Flavors.compile(getDependencies(), ‘biz-user‘)
}

这样的方式在我把Flavors这个类放在每个模块中的时候还是好用的, 但是这不是我的目的, 我的目的是它至少可以放到根项目的build.gradle中, 很显然, 我失败了, 原因是DependencyHandler这个玩意找不到.

既然这种方式不行, 那接下来我在yanbober提醒下, 很快的想到了第二种方式, 如果那么辅助类只给提供渠道信息呢? compile语句不做封装, Flavors提供一个current方法用来提供当前的渠道信息, 似乎这次不错, 大体代码是这样的.

// project的build.gradle
ext {
  flavors = new Flavors()
}
class Flavors {
    static String flavorType = ""

    def onTaskEach(String task) {
        if(task.contains("GooglePlay")){
            flavorType = "GooglePlay"
        } else if (task.contains("Qihoo360")){
            flavorType = "Qihoo360"
        }
    }

    def current(String lib) {
      if (flavorType == null || flavorType == "") { return lib}
      return ${lib}_${flavorType.toLowerCase()}
    }
}

// module的buld.gradle
gradle.startParameter.getTaskNames().each { task ->
    if (task.startsWith(":app")) {
        rootProject.ext.flavors.onTaskEach(task)
    }
}

dependencies {
  compile(name: rootProject.ext.flavors.current(‘biz-user‘), ext: ‘aar‘)
}

这种方式在大部分情况下是ok的, 当然我也兴奋了一小会, 但是在拿到同事电脑上用的时候, 死活就是报错, 后来我发现这种方式在我第一打开项目的时候flavorType还没拿到就进行compile了.

发现不行后, 我就忍痛删代码了. 既然这种方式走不通了, 那就退而求其次吧, 接下来我写了一个studio插件, 通过快捷键来生成分渠道的compile, 很方便, 至少还是解决了部分问题–人力问题, 但是还是不能解决’添加渠道后, 添加渠道compile语句’和代码丑这个问题. 这个插件在我们项目中现在还是保留着, 因为我们的项目中使用的网址是按照渠道分发的, 而且网络框架是Glin, 一个类似retrofit的东西, 所以网址必须是一个字面常量, 这样封装的constant必须按照按照渠道来打aar, 业务包也必须按照渠道来打包, 这一点在我分析后发现是不能逃避的, 所以这也给我定了方向, 接下来我的目标就是干掉UI层模块的多渠道.

最终实现

在又经过多种方案尝试后, 我有了一个疯狂的想法–自己模拟实现一下多渠道, 而且最终也被我实现了(要不然也不会有本文了…), 最后总结了一下, 很多时候我们的思路都被现有的大家都这么用的方式给束缚住了, 我们需要大胆的想象, 必要的时候可以打破这种束缚.

要自己去模拟多渠道, 那么需要解决的问题如下,

  1. 实现全量渠道包的生成.
  2. 实现单个渠道包的生成.
  3. 实现渠道的切换.
  4. 实现manifestPlaceholders功能

第一点是我们要实现的基础功能. 第一点如果能实现, 第二点就不是问题了. 第三点是为了直接run的时候能统一渠道而必须要实现的, 想一下, 如果测试同事拿着手机和数据线来找你跑一个最新版的时候, 你总不能打出所有包来再给人家安装吧. 第四点的需求没有那么强烈, 但是也有可能会用到, 所以我放到最后去实现的.

先来说说我的思路吧, 使用命令gradlew assembleDebug可以打出一个包来, 那么我就用一个循环来打包, 打一次包, 将compile给替换一下, 继续打包, 直到我们的渠道遍历完为止, 当然了, 这个替换过程是全自动的.

那如果替换compile呢? 我的做法是在compile之前给我一个特定的标识, 所以我们的compile长这样.

/**rep*/compile(name:‘biz-user_google_play‘, ext: ‘aar‘)

这样, 在打包的时候, 我发现/**rep*/标识后的compile会进行动态替换.

接下来, 我们先来看看新的实现是如果使用的.

gradlew publish

用来实现上面问题1.

gradlew publishDefault

用来实现上面问题2, 但是它会依赖我们一个配置值.

gradlew chVar

用来实现上面问题3

上面3个命令对应了3个gradle task.

所以先来创建3个task, 在看代码之前, 我们先来看看配置文件.

// publish_config.gradle
ext {
    defaultVariantIndex = 0

    variants = [‘GooglePlay‘,‘GooglePlayTest‘,‘Qihoo360‘,‘Qihoo360Test‘]
    apps = [‘demo‘]
}

这里的配置是需要手动修改的, 可以看到渠道信息是定义了一个variants列表.

3个task的代码如下,

// publish.gradle
ext {
    gradles = ‘/build.gradle‘
    rep = "/**rep*/"
    aarPat = ".*compile\\s*\\(name:\\s*[\"‘]\\s*([a-zA-Z0-9-]+?)_.*\\).*"
    placePat = "\\/\\*\\*place:\\s*(.*)\\s*\\*\\/"
}

tasks.create(name: ‘publish‘) << {
    for (variant in variants) {
        start variant
    }

    println "All finished, restore compile to default"
    repCompile variants[defaultVariantIndex]
}

tasks.create(name: ‘publishDefault‘) << {
    start variants[defaultVariantIndex]
}

tasks.create(name: ‘chVar‘) << {
    repCompile variants[defaultVariantIndex]
}

来看看publish这个task的代码, 首先我们来遍历所有的variant, 然后调用start函数, 最后执行完毕后, 还要将所有的build.gradle还原一下.

再来看看start这个函数,

def start(String variant) {
    repCompile variant

    def gradlewCmd = isWindows() ? "./gradlew.bat" : "gradlew"
    def proc = Runtime.getRuntime().exec "${gradlewCmd} assembleDebug"
    new StreamPrinter(proc.getInputStream(), "INFO").start()
    new StreamPrinter(proc.getErrorStream(), "ERROR").start()
    proc.waitFor()

    rename "-debug", "_${variant}"

    println "--------SUCCESS--------"
}

首先调用了repCompile这个函数, 这个函数的作用肯定就是替换我们上面提到的语句了, 接下来, 执行了gradlew assembleDebug来打包, 打包完毕后还要重命名一下apk文件, 这样也是传统的渠道打包具有的功能, 这里rename函数实现了重命名的功能.

再来看看repCompile这个函数,

def repCompile(String v) {
    for (app in apps) {
        def path = "./${app}/"
        def current = file(path)
        current.eachFile { file ->
            if (file.isDirectory()) {
                repFile file.path + gradles, v
            }
        }
    }
}

这里遍历了一下所有的app, 然后拿到app下的文件进行遍历, 在遍历过程中又调用了repFile这个函数, 这个函数才是真正执行替换的地方.

def repFile(String f, String variant) {
    def file = file(f)
    def txt = file.text

    def lines = txt.readLines()
    def size = lines.size()

    for (def i = 0; i < size; i++) {
        def line = lines[i]

        def rep = repAAR line, variant
        if (rep != "") {
            txt = txt.replace line, rep
        }
    }

    file.write txt
}

这里首先是按行读取了每个module下的build.gradle文件, 然后调用repAAR函数, 这个函数的作用判断当前行是不是需要做替换, 如果需要会返回替换后的新内容, 接下来, 如果需要替换, 替换当前行, 最终再将新内容重新写入文件.

aar的compile的动态替换是这么替换的.

def repAAR(String line, String variant) {
    if (!line.trim().startsWith(rep)) {
        return ""
    }

    def pattern = java.util.regex.Pattern.compile aarPat
    def matcher = pattern.matcher line
    if (matcher.find()) {
        def libName = matcher.group 1
        return "    /**rep*/compile (name:‘${libName}_${variant.toLowerCase()}‘, ext:‘aar‘)"
    }

    return ""
}

这里首先判断当前行的内容是否配置aarPat这个正则, 如果匹配, 则根据当前的lib名称和当前遍历到的渠道名生成一个compile语句的字符串, 然后返回, 至于aarPat, 可以翻翻上文找到. 到这里, 我们就实现了compile语句的替换, 接下来就是重命名了.

def rename(String oldSuffix, String newSuffix) {
    def roots = project.getSubprojects()

    for (item in roots) {
        if (!projectInApps(item.getName())) { continue}

        def children = item.getSubprojects()
        if (children.isEmpty()) { continue}

        for (module in children) {
            def lineList = module.getBuildFile().readLines()
            if (lineList.get(0).contains("com.android.application"))  {
                def oldName = module.getName() + oldSuffix
                def newName = module.getName() + newSuffix

                def path = module.getBuildDir().getPath()

                def apk = file("${path}/outputs/apk/${oldName}.apk")
                def renameApk = file("${path}/outputs/apk/${newName}.apk")
                if (apk.exists()) {
                    if (renameApk.exists()) {
                        renameApk.delete()
                    }

                    println "rename ${apk.getName()} to ${renameApk.getName()}.apk"

                    apk.renameTo renameApk
                }
            }
        }
    }
}

这里的代码有点多, 具体的思路就是拿到所有的module, 然后再拿到该moudle下的build.gradle文件, 读取它的第一行, 看看是不是apply plugin: ‘com.android.application‘,毕竟只有application的module才会有apk生成, 最后判断它的build下是不是有apk-debug.apk文件, 如果有, 则进行重命名.

这样, 整个过程就完成了, publishDefaultchVar命令的过程, 也在这个过程中完成了, publishDefault就是拿出defaultVariantIndex对应的variant然后调用start函数, chVar就是拿出defaultVariantIndex对应的variant然后调用repCompile函数.

还有最后一个问题还有说到, 就是manifestPlaceholders的实现, 下面就来实现下这个功能. 参考compile的实现思路, manifestPlaceholders的实现也是替换的思路, 所以在使用的时候用这样的方式,

buildTypes {
  debug {
    /**place: GooglePlay,GooglePlayTest*/
    manifestPlaceholders = [VALUE: ‘google‘]
    /**place: Qihoo360,Qihoo360Test*/
    //manifestPlaceholders = [VALUE: ‘360‘]
  }
}

对于它的处理思路是, 在repFile函数按行读的时候, 判断该行是不是/**place:渠道1,渠道2...*/这样的方式, 如果匹配成功, 接着判断当前渠道是否在place的渠道列表中, 如果在, 则将下面一行的注释//删除掉, 如果不是, 在下面一行添加注释//, 具体的实现代码,

def repFile(String f, String variant) {
    def file = file(f)
    def txt = file.text

    def lines = txt.readLines()
    def size = lines.size()

    for (def i = 0; i < size; i++) {
        def line = lines[i]

        // 省略上面repAAR的代码

        def matched = matchPlace(line, variant)
        if (matched != 0 && i + 1 < size) {
            def nextLine = lines[i + 1]
            if (matched == 1) {
                if (!nextLine.trim().startsWith("//")) {
                    txt = txt.replace nextLine, "//" + nextLine
                }
            } else if (matched == 2) {
                if (nextLine.trim().startsWith("//")) {
                    txt = txt.replace nextLine, nextLine.replaceFirst("//", "")
                }
            }
        }
    }

    file.write txt
}

for循环里的代码, 首先调用matchPlace函数, 该函数具有int类型的返回值, 如果是0, 则表示不匹配, 如果是1, 则表示匹配成功但是当前渠道不再place渠道列表中, 如果是2, 则表示匹配成功并且当前渠道在place渠道列表中. 接下来就是对返回值的判断, 根据返回值matched的值来决定下一行是添加注释还是删除注释.

再来看看matchPlace函数的实现,

/** return 0 no matched,
 *  1 matched but not contains current variant,
 *  2 matched and contains current variant
 */
def matchPlace(String line, String variant) {
    def pattern = java.util.regex.Pattern.compile placePat
    def matcher = pattern.matcher line
    if (matcher.find()) {
        def vs = matcher.group 1
        def vsArray = vs.split ","
        if (inArray(variant, vsArray)) {
            return 2
        }

        return 1
    }

    return 0
}

这里的代码, 就是实现了上面的关于返回值定义的思路, 利用正则来匹配当前行, 根据匹配结果和渠道信息来决定返回结果.

到现在为止, 已经实现了对多渠道的基本功能的模拟, 而且, 也成功的干掉了UI模块的多渠道信息, 再也不用书写那么多渠道信息了, 模块的build.gradle里也清爽了, 再也不用手撕那么多XXXCompile了. 最后给出完整的代码, 如果需要, 根据项目结构稍作修改就可以使用了.

// publish_config.gradle
ext {
    defaultVariantIndex = 0

    variants = [‘GooglePlay‘,‘GooglePlayTest‘,‘Qihoo360‘,‘Qihoo360Test‘]
    apps = [‘demo‘]
}

// publish.gradle
/**
 * gradlew publish \n
 * gradlew publishDefault \n
 * gradlew chVar \n
 */
apply from: ‘./publish_config.gradle‘

ext {
    gradles = ‘/build.gradle‘
    rep = "/**rep*/"
    aarPat = ".*compile\\s*\\(name:\\s*[\"‘]\\s*([a-zA-Z0-9-]+?)_.*\\).*"
    placePat = "\\/\\*\\*place:\\s*(.*)\\s*\\*\\/"
}

tasks.create(name: ‘publish‘) << {
    for (variant in variants) {
        start variant
    }

    println "All finished, restore compile to default"
    repCompile variants[defaultVariantIndex]
}

tasks.create(name: ‘publishDefault‘) << {
    start variants[defaultVariantIndex]
}

tasks.create(name: ‘chVar‘) << {
    repCompile variants[defaultVariantIndex]
}

def start(String variant) {
    repCompile variant

    def gradlewCmd = isWindows() ? "./gradlew.bat" : "gradlew"
    def proc = Runtime.getRuntime().exec "${gradlewCmd} assembleDebug"
    new StreamPrinter(proc.getInputStream(), "INFO").start()
    new StreamPrinter(proc.getErrorStream(), "ERROR").start()
    proc.waitFor()

    rename "-debug", "_${variant}"

    println "--------SUCCESS--------"
}

def repCompile(String v) {
    for (app in apps) {
        def path = "./${app}/"
        def current = file(path)
        current.eachFile { file ->
            if (file.isDirectory()) {
                repFile file.path + gradles, v
            }
        }
    }
}

def repFile(String f, String variant) {
    def file = file(f)
    def txt = file.text

    def lines = txt.readLines()
    def size = lines.size()

    for (def i = 0; i < size; i++) {
        def line = lines[i]

        def rep = repAAR line, variant
        if (rep != "") {
            txt = txt.replace line, rep
        }

        def matched = matchPlace(line, variant)
        if (matched != 0 && i + 1 < size) {
            def nextLine = lines[i + 1]
            if (matched == 1) {
                if (!nextLine.trim().startsWith("//")) {
                    txt = txt.replace nextLine, "//" + nextLine
                }
            } else if (matched == 2) {
                if (nextLine.trim().startsWith("//")) {
                    txt = txt.replace nextLine, nextLine.replaceFirst("//", "")
                }
            }
        }
    }

    file.write txt
}

/** return 0 no matched,
 *  1 matched but not contains current variant,
 *  2 matched and contains current variant*/
def matchPlace(String line, String variant) {
    def pattern = java.util.regex.Pattern.compile placePat
    def matcher = pattern.matcher line
    if (matcher.find()) {
        def vs = matcher.group 1
        def vsArray = vs.split ","
        if (inArray(variant, vsArray)) {
            return 2
        }

        return 1
    }

    return 0
}

def repAAR(String line, String variant) {
    if (!line.trim().startsWith(rep)) {
        return ""
    }

    def pattern = java.util.regex.Pattern.compile aarPat
    def matcher = pattern.matcher line
    if (matcher.find()) {
        def libName = matcher.group 1
        return "    /**rep*/compile (name:‘${libName}_${variant.toLowerCase()}‘, ext:‘aar‘)"
    }

    return ""
}

def rename(String oldSuffix, String newSuffix) {
    def roots = project.getSubprojects()

    for (item in roots) {
        if (!projectInApps(item.getName())) { continue}

        def children = item.getSubprojects()
        if (children.isEmpty()) { continue}

        for (module in children) {
            def lineList = module.getBuildFile().readLines()
            if (lineList.get(0).contains("com.android.application"))  {
                def oldName = module.getName() + oldSuffix
                def newName = module.getName() + newSuffix

                def path = module.getBuildDir().getPath()

                def apk = file("${path}/outputs/apk/${oldName}.apk")
                def renameApk = file("${path}/outputs/apk/${newName}.apk")
                if (apk.exists()) {
                    if (renameApk.exists()) {
                        renameApk.delete()
                    }

                    println "rename ${apk.getName()} to ${renameApk.getName()}.apk"

                    apk.renameTo renameApk
                }
            }
        }
    }
}

def projectInApps(String name) {
    for (app in apps) {
        if (name.equals(app)) { return true}
    }
    return false
}

def isWindows() {
    def osName = System.getProperty("os.name").toLowerCase()
    def result = osName.startsWith "windows"
    return result
}

def inArray(String str, String[] array) {
    for (item in array) {
        if (item.trim().equals(str)) { return true}
    }

    return false
}

public class StreamPrinter extends Thread {
    private InputStream inputStream
    private String type

    StreamPrinter(InputStream inputStream, String type) {
        this.inputStream = inputStream
        this.type = type
    }

    public void run() {
        BufferedReader br
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream)
            br = new BufferedReader(inputStreamReader)
            String line = null
            while ((line = br.readLine()) != null) {
                println type + " > " + line

                if (line.toLowerCase().contains("build failed")) {
                    println "exit..."

                    System.exit 1
                }

            }
        } catch (IOException e) {
            e.printStackTrace()
        } finally {
            if (br != null) {
                try {
                    br.close()
                } catch (Exception e2) {
                    e2.printStackTrace()
                }
            }
        }
    }
}

最后别忘了在项目的build.gradle中添加一下代码,

apply from: ‘./publish.gradle‘
时间: 2024-11-03 01:29:12

当模块化遇上多渠道的相关文章

当css遇上less

某种程度来讲,css不能称得上程序.虽然它也和其它语言一样,有自己的规范,也需要编码,但它的笨拙实在让我失望.不喜欢css是因为无论怎么优化代码,项目大到一定程序后,都会看上去一团乱.而且有时候一个bug的定位也要花去不少时间.直到我发现了less.突然感慨,css中的jquery大概就是它了. less允许传参数,允许定义变量,可以把层叠的样式组织得较为美观,可以少写许多重复代码--这一切的优势,让我毫不犹豫地要去把它加入接下来的项目. 举个例子: css要这样写: #header h1 {

当VB遇上C++

最近在学习VB.NET 这块的东西,自然而然就会想到VB.NET与VB6是什么关系? 宏观上来讲就是从基于对象变成了完全的面向对象,因此不能简单的说VB.NET是VB6.0的升级版本.在学习VB.NET之前,已经接触过C++和VB,所以在学习VB.NET的时候总能看到他们的影子,那种似曾相识的感觉让我不得不仰天长叹:这VB.NET简直就是混搭版本的程序设计语言啊! 在1991年Visual Basic1.0诞生以前,开发人员不得不使用C++和Windows系统本身的未成形的程序块,即所谓的Win

渗透场景篇--当XSS遇上CSRF

你是否有过这样的经历,你发现了一个xss,但是貌似只能叉自己,输出点只有自己可以看见.这个时候,你会觉得这个xss很鸡肋,当你就此忽略这个漏洞的时候,你可能丢掉一个发出组合技能的机会.    今天我们来介绍一个场景,当xss遇上csrf的时候,是否能打出一套漂亮的组合技能. 实验环境:     ZvulDirll[请用下面我简单修改过的版本]     下载地址:在文章最后面 一.安装:0x00:解压ZVulDrill压缩包,将其放在www目录下,也就是你的网站根目录.0x01.编辑ZVulDri

敏捷遇上UML-需求分析及软件设计最佳实践(郑州站 2014-6-7)

邀请函:尊敬的阁下:我们将在郑州为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实战技巧.时间:2014.06.07(周六),上午9:00-12:00,下午14:00-17:30(时长6.5小时)地点:郑州市畜牧路16号牧业经济学院实验楼B座2518(可乘坐B11.909.962.47路等公交车到老长途汽车北站下车畜牧路向东300米路北)软件知识原创基地www.umlonline.or

当property遇上category

[当property遇上category] @property可以在类定义中,以及extension定义中使用,编译器会自动为@property生成代码,并在变量列表(ivar_list_t)中添加相应的以下划线开头的变量. 在category中,编译器允许定义@property,但不会为此@property生成代码,也即意味着编译器不会在变量列表中加入property的变量.必须人工的实现property的方法. 参考:https://developer.apple.com/library/m

当数据库遇上云计算 网亿兴云解决方案

数据库遇上云计算 网亿兴云解决方案 [日期:2016-07-21] 来源: 中关村在线  作者: [字体:大 中 小] 人们对数据管理的需求由来已久.1950年,雷明顿兰德公司在"Univac I"计算机上推出了磁带驱动器,每秒可以输入数百条记录.六十年代,计算机开始广泛引用于数据管理,传统的文件系统已经不能满足人们的需要,能够统一管理和共享数据的数据库管理系统应运而生.如今,数据已经不再是简单的储存和管理,基于云的数据库正衍生出越来越多的玩法和应用场景. 数据库遇上云计算 网亿兴云解

当linux遇上多网卡时

我虚拟机有三个网卡,有两个在用,分别是不同的网段 eth4: 192.168.100.6/24 eth6: 192.168.137.131/24 但是默认系统把我的内部网络eth6设置为所有ip地址需要经过的地方,如下 ~ $ip route192.168.100.0/24 dev eth4 proto kernel scope link src 192.168.100.6192.168.137.0/24 dev eth6 proto kernel scope link src 192.168.

当Azure遇上Docker

容器技术现在发展的如火如荼,包括微软也会在下一代操作系统中提供原生支持,个人认为微软的优势在于其拥有成熟的集群工作环境(Nano Server)以及成熟的管理平台(System Center),除此之外还有一系列围绕容器的生态链,比如自动化(PowerShell DSC),比如监控,比如生命周期管理等等.当然目前如果想在Win平台上体验容器的话,除了Docker提供的boot2docker,最"土"的办法就是在Hyper-V里创建一台Linux虚拟机,然后去部署并使用Docker环境.

当肿瘤遇上VR眼镜,却只能乖乖滚蛋

<滚蛋吧!肿瘤君>由白百何.吴彦祖领衔主演,改编自漫画师熊顿的同名漫画.影片里,患癌女主是年轻的乐天派漫画家,她在人生最艰难的时刻笑对命运.近年来,恶性肿瘤发病率越来越高,人们谈"癌"色变.但是当肿瘤君遇上VR眼镜,却只能乖乖滚蛋. 虚拟现实(VR)已不再是虚无缥缈的概念,更不是戴上头显看看视频.打打游戏那么简单.其实,VR与大部分传统行业进行碰撞,都将产生摧枯拉朽般的变革.今天这个故事讲述的是虚拟现实与传统医疗交汇出的新世界. 肿瘤位置特殊,周围神经密布,切除难度大.戴上