IT牛人博客聚合网站 发现IT技术最优秀的内容, 寻找IT技术的价值 http://www.udpwork.com/ zh_CN http://www.udpwork.com/about hourly 1 Wed, 18 Jan 2017 20:08:40 +0800 <![CDATA[10 连抽保底的概率模型]]> http://www.udpwork.com/item/16064.html http://www.udpwork.com/item/16064.html#reviews Mon, 16 Jan 2017 23:18:15 +0800 云风 http://www.udpwork.com/item/16064.html 网游里有很多抽卡、开箱子之类的赌性玩法,在最开始,游戏设计者实现的时候,仅仅给这些抽取概率简单的设置了一个值。比如抽卡抽出橙卡的概率是 10% ,那么就是说,玩家每次抽一张卡,有 90% 的可能是白卡,10% 的可能是橙卡。

但大 R 玩家是大爷,需要小心伺候。如果感受不好,人跑了就亏大了。概率这个东西靠改进伪随机数发生器是解决不了体验问题的,大爷要是连抽 20 张都出不来橙卡,那是要怒删游戏的。

连抽 20 张 10% 概率橙卡一张都抽不到的机会多不?一张抽不中的概率是 0.9 ,20 张都抽不中的概率是 0.9 ^20 = 12.2% 。这可不算小数字啊。平均 8 个大 R 就会碰到一次,一下子赶跑了 1/8 的金主,这个责任小策划可担当不起。

所以、一般网游都会用各种规则来避免玩家出现连抽不中的尴尬。例如,我忘记是谁发明的 10 连抽规则:如果你购买一个大包连抽 10 次,我在规则上就保证里面一定至少有一张橙卡。实现它应该并不困难,按常规概率生成 10 张的卡包,如果里面没有橙卡,那么我加一张即可。

但如果我想把 10 抽保底的规则惠及日常抽卡的玩家该怎么做呢?

就是说,我希望任何玩家任何时候,接连抽了 10 张卡,我都想保证这 10 张卡里至少有一张橙卡。

首先,要说明的一点:如果你同时想保证橙卡掉落率是 10% ,也就是在极大范围内,比如系统投放了一万张卡片中,其中要有一千张橙卡。那么同时保证每 10 张卡里有至少一张橙卡的话,结果一定是完全不随机的,也就是必须每抽 9 张白卡,必出一张橙卡。

所以、如果即想要随机(出橙卡的概率稳定),又想有 10 张出一张的保底,那么橙卡投放量是一定超过 1/10 的。

我们之前的游戏用了个很粗暴的方案:记录玩家已经连续几次没有抽中,如果连续次数超过 9 ,就必给他一张橙卡。为什么我说这个方案粗暴,因为它其实破坏了抽卡的自然体验。虽然玩家的确更高兴了,但是概率却很不自然。不自然的方案(其实是生硬的打了个补丁)实现起来还容易出错,我们前段时间就因为实现 bug 多发放了很多稀有物品,这个 bug 就不展开说了。

下面来看看,为什么这么做不自然。

假设橙卡的掉率是 10% ,那么你在获得一张橙卡后,再抽下一张橙卡的概率就是 0.1 。下一张是白卡,再下一张是橙卡的概率是 0.9 * 0.1 ,下两张是白卡,第三张是橙卡的概率是 0.9^2 * 0.1 ……

后续有 10 张及 10 张以上的概率总共有多少呢?我算了一下,大约是 35% 左右。

我们把抽到两张橙卡之间会抽取到的白卡张数排成一个数列的话,这个数列的值的范围是 0 到正无穷。是的,非洲酋长可能永远抽不到橙卡。当然这只是理论值。

如果你读过大学,学的是理工科,没有逃课的话,就应该知道,这个数列是大致符合指数分布的。指数分布正是用来表示独立随机事件发生的时间间隔的。

当我们把这个数列中大于 9 的数字都强行改成 9 ,那么 9 的出现频率就陡然跳变,这是极不自然的。(分布不平滑)

从一致分布的随机数,转换为指数分布的随机数非常简单。如果你懒得自己推导,那么可以在爆栈网上找到公式

让我们回答前面的问题,如果我希望获得一个大约每 10 张卡里出一张橙卡的随机数列,除了每次 random 一个 [0,10) 的整数,判断证书是不是 0 以外,还有一个方法。那就是每次抽到一个橙卡后,都从一个指数分布的随机数列中取一个值出来,作为接下来会抽取到白卡的张数。按这个张数去发放白卡,等计数器减到 0 ,就发一张橙卡给玩家。这个白卡张数的数值范围是 [0, inf) 。

用 lua 实现的话,大概是这样的:

math.floor(math.log(1-math.random()) * (-rate))其中 rate = 10 。

好了,如果我们想加上 10 张保底,又想让间隔大致符合指数分布怎么办?简单:

function erand(rate)
    while true do
        local p = math.floor(math.log(1-math.random()) * (-rate))
        if p 

让产生出来的数字小于 10 的时候重来一次就好了。如果你担心这里死循环(实际并不会),也可以加上循环上限:


function erand(rate)
    for i = 1, 100 do  -- 100 可以随便写
        local p = math.floor(math.log(1-math.random()) * (-rate))
        if p 

当然,一旦加上了 10 张保底,单张出橙卡的概率就大大增加了,增加到多少呢?大约是 21%。如果你希望保持 10% 左右的投放率,那么保底张数大约应该设置在 23 张左右。

ps. 今天在公司群里讨论这个问题时,雷先生提了这么一个问题,说是可以用来做数值策划的面试题:

已知橙卡的抽取率是 10% ,抽一次卡是 1 块钱;而 10 连抽的包可以帮你按同样概率连抽 10 次,但如果没有抽到橙卡的话,系统会补偿一张橙卡给你,换掉 10 张白卡中的一张。

假设白色一文不值,只有橙卡值钱。

那么请问:这个 10 连抽的包到底价值多少?
]]>
网游里有很多抽卡、开箱子之类的赌性玩法,在最开始,游戏设计者实现的时候,仅仅给这些抽取概率简单的设置了一个值。比如抽卡抽出橙卡的概率是 10% ,那么就是说,玩家每次抽一张卡,有 90% 的可能是白卡,10% 的可能是橙卡。

但大 R 玩家是大爷,需要小心伺候。如果感受不好,人跑了就亏大了。概率这个东西靠改进伪随机数发生器是解决不了体验问题的,大爷要是连抽 20 张都出不来橙卡,那是要怒删游戏的。

连抽 20 张 10% 概率橙卡一张都抽不到的机会多不?一张抽不中的概率是 0.9 ,20 张都抽不中的概率是 0.9 ^20 = 12.2% 。这可不算小数字啊。平均 8 个大 R 就会碰到一次,一下子赶跑了 1/8 的金主,这个责任小策划可担当不起。

所以、一般网游都会用各种规则来避免玩家出现连抽不中的尴尬。例如,我忘记是谁发明的 10 连抽规则:如果你购买一个大包连抽 10 次,我在规则上就保证里面一定至少有一张橙卡。实现它应该并不困难,按常规概率生成 10 张的卡包,如果里面没有橙卡,那么我加一张即可。

但如果我想把 10 抽保底的规则惠及日常抽卡的玩家该怎么做呢?

就是说,我希望任何玩家任何时候,接连抽了 10 张卡,我都想保证这 10 张卡里至少有一张橙卡。

首先,要说明的一点:如果你同时想保证橙卡掉落率是 10% ,也就是在极大范围内,比如系统投放了一万张卡片中,其中要有一千张橙卡。那么同时保证每 10 张卡里有至少一张橙卡的话,结果一定是完全不随机的,也就是必须每抽 9 张白卡,必出一张橙卡。

所以、如果即想要随机(出橙卡的概率稳定),又想有 10 张出一张的保底,那么橙卡投放量是一定超过 1/10 的。

我们之前的游戏用了个很粗暴的方案:记录玩家已经连续几次没有抽中,如果连续次数超过 9 ,就必给他一张橙卡。为什么我说这个方案粗暴,因为它其实破坏了抽卡的自然体验。虽然玩家的确更高兴了,但是概率却很不自然。不自然的方案(其实是生硬的打了个补丁)实现起来还容易出错,我们前段时间就因为实现 bug 多发放了很多稀有物品,这个 bug 就不展开说了。

下面来看看,为什么这么做不自然。

假设橙卡的掉率是 10% ,那么你在获得一张橙卡后,再抽下一张橙卡的概率就是 0.1 。下一张是白卡,再下一张是橙卡的概率是 0.9 * 0.1 ,下两张是白卡,第三张是橙卡的概率是 0.9^2 * 0.1 ……

后续有 10 张及 10 张以上的概率总共有多少呢?我算了一下,大约是 35% 左右。

我们把抽到两张橙卡之间会抽取到的白卡张数排成一个数列的话,这个数列的值的范围是 0 到正无穷。是的,非洲酋长可能永远抽不到橙卡。当然这只是理论值。

如果你读过大学,学的是理工科,没有逃课的话,就应该知道,这个数列是大致符合指数分布的。指数分布正是用来表示独立随机事件发生的时间间隔的。

当我们把这个数列中大于 9 的数字都强行改成 9 ,那么 9 的出现频率就陡然跳变,这是极不自然的。(分布不平滑)

从一致分布的随机数,转换为指数分布的随机数非常简单。如果你懒得自己推导,那么可以在爆栈网上找到公式

让我们回答前面的问题,如果我希望获得一个大约每 10 张卡里出一张橙卡的随机数列,除了每次 random 一个 [0,10) 的整数,判断证书是不是 0 以外,还有一个方法。那就是每次抽到一个橙卡后,都从一个指数分布的随机数列中取一个值出来,作为接下来会抽取到白卡的张数。按这个张数去发放白卡,等计数器减到 0 ,就发一张橙卡给玩家。这个白卡张数的数值范围是 [0, inf) 。

用 lua 实现的话,大概是这样的:

math.floor(math.log(1-math.random()) * (-rate))其中 rate = 10 。

好了,如果我们想加上 10 张保底,又想让间隔大致符合指数分布怎么办?简单:

function erand(rate)
    while true do
        local p = math.floor(math.log(1-math.random()) * (-rate))
        if p 

让产生出来的数字小于 10 的时候重来一次就好了。如果你担心这里死循环(实际并不会),也可以加上循环上限:


function erand(rate)
    for i = 1, 100 do  -- 100 可以随便写
        local p = math.floor(math.log(1-math.random()) * (-rate))
        if p 

当然,一旦加上了 10 张保底,单张出橙卡的概率就大大增加了,增加到多少呢?大约是 21%。如果你希望保持 10% 左右的投放率,那么保底张数大约应该设置在 23 张左右。

ps. 今天在公司群里讨论这个问题时,雷先生提了这么一个问题,说是可以用来做数值策划的面试题:

已知橙卡的抽取率是 10% ,抽一次卡是 1 块钱;而 10 连抽的包可以帮你按同样概率连抽 10 次,但如果没有抽到橙卡的话,系统会补偿一张橙卡给你,换掉 10 张白卡中的一张。

假设白色一文不值,只有橙卡值钱。

那么请问:这个 10 连抽的包到底价值多少?
]]>
0
<![CDATA[关于 Android 应用多进程的整理]]> http://www.udpwork.com/item/16063.html http://www.udpwork.com/item/16063.html#reviews Sun, 15 Jan 2017 20:38:00 +0800 技术小黑屋 http://www.udpwork.com/item/16063.html 在计算机操作系统中,进程是进行资源分配和调度的基本单位。这对于基于Linux内核的Android系统也不例外。在Android的设计中,一个应用默认有一个(主)进程。但是我们通过配置可以实现一个应用对应多个进程。

本文将试图对于Android中应用多进程做一些整理总结。

android:process

  • 应用实现多进程需要依赖于android:process这个属性
  • 适用元素:Application, Activity, BroadcastReceiver, Service, ContentProvider。
  • 通常情况下,这个属性的值应该是”:“开头。表示这个进程是应用私有的。无法在在跨应用之间共用。
  • 如果该属性值以小写字母开头,表示这个进程为全局进程。可以被多个应用共用。(文章结尾会探讨这个问题)

一个应用 android:process 简单示例

1
2
3
<activity android:name=".MusicPlayerActivity" android:process=":music"/>

<activity android:name=".AnotherActivity" android:process="droidyue.com"/>

应用多进程有什么好处

增加App可用内存

在Android中,默认情况下系统会为每个App分配一定大小的内存。比如从最早的16M到后面的32M或者48M等。具体的内存大小取决于硬件和系统版本。

这些有限的内存对于普通的App还算是够用,但是对于展示大量图片的应用来说,显得实在是捉襟见肘。

仔细研究一下,你会发现原来系统的这个限制是作用于进程的(毕竟进程是作为资源分配的基本单位)。意思就是说,如果一个应用实现多个进程,那么这个应用可以获得更多的内存。

于是,增加App可用内存成了应用多进程的重要原因。

独立于主进程

除了增加App可用内存之外,确保使用多进程,可以独立于主进程,确保某些任务的执行和完成。

举一个简单的例子,之前的一个项目存在退出的功能,其具体实现为杀掉进程。为了保证某些统计数据上报正常,不受当前进程退出的影响,我们可以使用独立的进程来完成。

多进程的不足与缺点

数据共享问题

  • 由于处于不同的进程导致了数据无法共享内容,无论是static变量还是单例模式的实现。
  • SharedPreferences 还没有增加对多进程的支持。
  • 跨进程共享数据可以通过Intent,Messenger,AIDL等。

SQLite容易被锁

  • 由于每个进程可能会使用各自的SQLOpenHelper实例,如果两个进程同时对数据库操作,则会发生SQLiteDatabaseLockedException等异常。
  • 解决方法:可以使用ContentProvider来实现或者使用其他存储方式。

不必要的初始化

  • 多进程之后,每个进程在创建的时候,都会执行自己的Application.onCreate方法。
  • 通常情况下,onCreate中包含了我们很多业务相关的初始化,更重要的这其中没有做按照进程按需初始化,即每个进程都会执行全部的初始化。
  • 按需初始化需要根据当前进程名称,进行最小需要的业务初始化。
  • 按需初始化可以选择简单的if else判断,也可以结合工厂模式

一些简单的代码示例

获取当前的进程名

1
2
3
4
5
6
7
8
9
10
11
12
private String getCurrentProcessName() {
    String currentProcName = "";
    int pid = android.os.Process.myPid();
    ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
    for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
        if (processInfo.pid == pid) {
            currentProcName = processInfo.processName;
            break;
        }
    }
    return currentProcName;
}

基本的进程初始化类

这个类用来每个进程共用的业务初始化逻辑。

1
2
3
4
5
6
public class AppInitialization {
    @CallSuper
    public void onAppCreate(Application application) {
        Log.i("AppInitialization", "onAppCreate is being executed.");
    }
}

工厂模式的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class AppInitFactory {
    public static AppInitialization getAppInitialization(String processName) {
        AppInitialization appInitialization;
        if (processName.endsWith(":game")) {
            appInitialization = new GameAppInitialization();
        } else if (processName.endsWith(":music")) {
            appInitialization = new MusicAppInitialization();
        } else {
            appInitialization = new AppInitialization();
        }
        return appInitialization;
    }

    static class GameAppInitialization extends AppInitialization {
        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            Log.i("GameAppInitialization", "onAppCreate is being executed.");
        }
    }

    static class MusicAppInitialization extends AppInitialization {
        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            Log.i("MusicAppInitialization", "onAppCreate is being executed.");
        }
    }
}

具体的调用时的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyApplication extends Application{
    private static final String LOGTAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        String currentProcessName = getCurrentProcessName();
        Log.i(LOGTAG, "onCreate currentProcessName=" + currentProcessName);
        AppInitialization appInitialization = AppInitFactory.getAppInitialization(currentProcessName);
        if (appInitialization != null) {
            appInitialization.onAppCreate(this);
        }
    }
}

是否需要多进程

判断是否需要多进程,需要视具体情况而定。

内存限制

  • 研究内存占用居高不下的原因
  • 如果是由内存泄漏导致,尝试解决来降低内存占用
  • 如有必要,可以通过配置largeHeap尝试解决

除了内存限制之外,还需要考虑是否真的需要独立于主进程来执行某些操作。

关于android:process的其他问题

在android:process部分我们提到,如果这个属性值以小写字母开头,那么就是全局的进程,可以被其他应用共用。

所谓的共用,指的是不同的App的组件运行在同一个指定的进程中。

准备条件

受制于Android系统的安全机制,我们需要做到以下两个准备条件才可以。

  • 这个应用使用同样的签名
  • 两个应用指定同一个android:sharedUserId的值

具体示例

第一个App的Manifest文件,AnotherActivity运行在名为droidyue.com的进程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.droidyue.androidmutipleprocesssample"
        android:sharedUserId="droidyue.com"
    >

    <application
            android:name=".MyApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">

        <activity android:name=".AnotherActivity" android:process="droidyue.com"/>
    </application>

</manifest>

第二个App的Manifest文件,SecondActivity运行在名为droidyue.com的进程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.jishuxiaoheiwu.accessfromanotherprocess"
    android:sharedUserId="droidyue.com"
    >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".SecondActivity"
            android:process="droidyue.com"
            />
    </application>

</manifest>

上面的AnotherActivity和SecondActivity会运行在一个名为droidyue.com的进程中,尽管他们位于不同的App中。

但是这种共用进程的方式会引发很多问题,不太建议大家使用。

以上就是我关于Android中多进程的一些浅显的研究,如有问题,欢迎指正。

]]>
在计算机操作系统中,进程是进行资源分配和调度的基本单位。这对于基于Linux内核的Android系统也不例外。在Android的设计中,一个应用默认有一个(主)进程。但是我们通过配置可以实现一个应用对应多个进程。

本文将试图对于Android中应用多进程做一些整理总结。

android:process

  • 应用实现多进程需要依赖于android:process这个属性
  • 适用元素:Application, Activity, BroadcastReceiver, Service, ContentProvider。
  • 通常情况下,这个属性的值应该是”:“开头。表示这个进程是应用私有的。无法在在跨应用之间共用。
  • 如果该属性值以小写字母开头,表示这个进程为全局进程。可以被多个应用共用。(文章结尾会探讨这个问题)

一个应用 android:process 简单示例

1
2
3
<activity android:name=".MusicPlayerActivity" android:process=":music"/>

<activity android:name=".AnotherActivity" android:process="droidyue.com"/>

应用多进程有什么好处

增加App可用内存

在Android中,默认情况下系统会为每个App分配一定大小的内存。比如从最早的16M到后面的32M或者48M等。具体的内存大小取决于硬件和系统版本。

这些有限的内存对于普通的App还算是够用,但是对于展示大量图片的应用来说,显得实在是捉襟见肘。

仔细研究一下,你会发现原来系统的这个限制是作用于进程的(毕竟进程是作为资源分配的基本单位)。意思就是说,如果一个应用实现多个进程,那么这个应用可以获得更多的内存。

于是,增加App可用内存成了应用多进程的重要原因。

独立于主进程

除了增加App可用内存之外,确保使用多进程,可以独立于主进程,确保某些任务的执行和完成。

举一个简单的例子,之前的一个项目存在退出的功能,其具体实现为杀掉进程。为了保证某些统计数据上报正常,不受当前进程退出的影响,我们可以使用独立的进程来完成。

多进程的不足与缺点

数据共享问题

  • 由于处于不同的进程导致了数据无法共享内容,无论是static变量还是单例模式的实现。
  • SharedPreferences 还没有增加对多进程的支持。
  • 跨进程共享数据可以通过Intent,Messenger,AIDL等。

SQLite容易被锁

  • 由于每个进程可能会使用各自的SQLOpenHelper实例,如果两个进程同时对数据库操作,则会发生SQLiteDatabaseLockedException等异常。
  • 解决方法:可以使用ContentProvider来实现或者使用其他存储方式。

不必要的初始化

  • 多进程之后,每个进程在创建的时候,都会执行自己的Application.onCreate方法。
  • 通常情况下,onCreate中包含了我们很多业务相关的初始化,更重要的这其中没有做按照进程按需初始化,即每个进程都会执行全部的初始化。
  • 按需初始化需要根据当前进程名称,进行最小需要的业务初始化。
  • 按需初始化可以选择简单的if else判断,也可以结合工厂模式

一些简单的代码示例

获取当前的进程名

1
2
3
4
5
6
7
8
9
10
11
12
private String getCurrentProcessName() {
    String currentProcName = "";
    int pid = android.os.Process.myPid();
    ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
    for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
        if (processInfo.pid == pid) {
            currentProcName = processInfo.processName;
            break;
        }
    }
    return currentProcName;
}

基本的进程初始化类

这个类用来每个进程共用的业务初始化逻辑。

1
2
3
4
5
6
public class AppInitialization {
    @CallSuper
    public void onAppCreate(Application application) {
        Log.i("AppInitialization", "onAppCreate is being executed.");
    }
}

工厂模式的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class AppInitFactory {
    public static AppInitialization getAppInitialization(String processName) {
        AppInitialization appInitialization;
        if (processName.endsWith(":game")) {
            appInitialization = new GameAppInitialization();
        } else if (processName.endsWith(":music")) {
            appInitialization = new MusicAppInitialization();
        } else {
            appInitialization = new AppInitialization();
        }
        return appInitialization;
    }

    static class GameAppInitialization extends AppInitialization {
        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            Log.i("GameAppInitialization", "onAppCreate is being executed.");
        }
    }

    static class MusicAppInitialization extends AppInitialization {
        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            Log.i("MusicAppInitialization", "onAppCreate is being executed.");
        }
    }
}

具体的调用时的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyApplication extends Application{
    private static final String LOGTAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        String currentProcessName = getCurrentProcessName();
        Log.i(LOGTAG, "onCreate currentProcessName=" + currentProcessName);
        AppInitialization appInitialization = AppInitFactory.getAppInitialization(currentProcessName);
        if (appInitialization != null) {
            appInitialization.onAppCreate(this);
        }
    }
}

是否需要多进程

判断是否需要多进程,需要视具体情况而定。

内存限制

  • 研究内存占用居高不下的原因
  • 如果是由内存泄漏导致,尝试解决来降低内存占用
  • 如有必要,可以通过配置largeHeap尝试解决

除了内存限制之外,还需要考虑是否真的需要独立于主进程来执行某些操作。

关于android:process的其他问题

在android:process部分我们提到,如果这个属性值以小写字母开头,那么就是全局的进程,可以被其他应用共用。

所谓的共用,指的是不同的App的组件运行在同一个指定的进程中。

准备条件

受制于Android系统的安全机制,我们需要做到以下两个准备条件才可以。

  • 这个应用使用同样的签名
  • 两个应用指定同一个android:sharedUserId的值

具体示例

第一个App的Manifest文件,AnotherActivity运行在名为droidyue.com的进程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.droidyue.androidmutipleprocesssample"
        android:sharedUserId="droidyue.com"
    >

    <application
            android:name=".MyApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">

        <activity android:name=".AnotherActivity" android:process="droidyue.com"/>
    </application>

</manifest>

第二个App的Manifest文件,SecondActivity运行在名为droidyue.com的进程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.jishuxiaoheiwu.accessfromanotherprocess"
    android:sharedUserId="droidyue.com"
    >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".SecondActivity"
            android:process="droidyue.com"
            />
    </application>

</manifest>

上面的AnotherActivity和SecondActivity会运行在一个名为droidyue.com的进程中,尽管他们位于不同的App中。

但是这种共用进程的方式会引发很多问题,不太建议大家使用。

以上就是我关于Android中多进程的一些浅显的研究,如有问题,欢迎指正。

]]>
0
<![CDATA[豆豆的日常]]> http://www.udpwork.com/item/16062.html http://www.udpwork.com/item/16062.html#reviews Sat, 14 Jan 2017 20:17:01 +0800 云风 http://www.udpwork.com/item/16062.html 云豆两岁的日常:

奶奶:10 点了,豆豆该睡觉了。

豆豆:要听咕哩咕哩。

奶奶:ipad 没电了。

(ipad 电量 1%)

豆豆:ipad 真没电了。

奶奶:听小兔子好吗?

豆豆:(拧开开关)小兔子不好听。(关上)

豆豆:ipad 没电了、手机也没电了、奶奶唱歌。

奶奶:唱什么?

豆豆:门前大桥下……

奶奶:我不记得歌词了。

豆豆:(唱)门前大桥下,游过一群鸭…… 不记得歌词了。

豆豆:奶奶唱,(唱)小燕子、 穿花衣,年年春天来这里~

奶奶:你不是会唱吗?

豆豆: 奶奶唱小燕子, 豆豆睡觉。

奶奶:(唱)小燕子 穿花衣 年年春天来这里~ Zzz...

豆豆:奶奶睡着啦。

]]>
云豆两岁的日常:

奶奶:10 点了,豆豆该睡觉了。

豆豆:要听咕哩咕哩。

奶奶:ipad 没电了。

(ipad 电量 1%)

豆豆:ipad 真没电了。

奶奶:听小兔子好吗?

豆豆:(拧开开关)小兔子不好听。(关上)

豆豆:ipad 没电了、手机也没电了、奶奶唱歌。

奶奶:唱什么?

豆豆:门前大桥下……

奶奶:我不记得歌词了。

豆豆:(唱)门前大桥下,游过一群鸭…… 不记得歌词了。

豆豆:奶奶唱,(唱)小燕子、 穿花衣,年年春天来这里~

奶奶:你不是会唱吗?

豆豆: 奶奶唱小燕子, 豆豆睡觉。

奶奶:(唱)小燕子 穿花衣 年年春天来这里~ Zzz...

豆豆:奶奶睡着啦。

]]>
0
<![CDATA[Laravel专供:实现Schemaless]]> http://www.udpwork.com/item/16061.html http://www.udpwork.com/item/16061.html#reviews Sat, 14 Jan 2017 17:11:03 +0800 老王 http://www.udpwork.com/item/16061.html 之所以要实现 Schemaless,主要是因为在线 DDL 有很多痛点,关于这一点,我在以前已经写过文章,没看过的不妨看看「史上最LOW的在线DDL解决方案」,不过那篇文章主要以介绍为主,并没有涉及具体的实现,所以我写了一个 Laravel 的例子。

首先创建测试用的 users 表,并且添加虚拟字段 name、address、level:

mysql> CREATE TABLE users (
           id INT UNSIGNED NOT NULL AUTO_INCREMENT,
           created_at timestamp null,
           updated_at timestamp null,
           data JSON NOT NULL,
           PRIMARY KEY(id)
       );

mysql> ALTER TABLE users add name VARCHAR(100) AS
       (JSON_UNQUOTE(JSON_EXTRACT(data, '$.name'))) AFTER id;

mysql> ALTER TABLE users add address VARCHAR(100) AS
       (JSON_UNQUOTE(JSON_EXTRACT(data, '$.address'))) AFTER name;

mysql> ALTER TABLE users add level INT UNSIGNED AS
       (JSON_EXTRACT(data, '$.level')) AFTER name;

然后是核心代码 Schemaless.php,以 trait 的方式实现:

<?php

namespace App;

trait Schemaless
{
    public function getDirty()
    {
        $dirty = collect(parent::getDirty());

        $keys = $dirty->keys()->map(function($key) {
            if (in_array($key, $this->virtual)) {
                $key = $this->getDataColumn() . '->' . $key;
            }

            return $key;
        });

        return $keys->combine($dirty)->all();
    }

    public function save(array $options = [])
    {
        if (!$this->exists) {
            $this->reviseRawAttributes();
        }

        return parent::save($options);
    }

    public function getDataColumn()
    {
        static $column;

        if ($column === null) {
            $column = defined('static::DATA') ? static::DATA : 'data';
        }

        return $column;
    }

    private function reviseRawAttributes()
    {
        $attributes = collect($this->getAttributes());

        $virtual = $attributes->only($this->virtual);

        $attributes = $attributes->diffKeys($virtual)->merge([
            $this->getDataColumn() => json_encode($virtual->all()),
        ]);

        $this->setRawAttributes($attributes->all());
    }
}

接着是 Model 实现 User.php,里面激活了 schemaless,并设置了虚拟字段:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use Schemaless;

    protected $virtual = ['name', 'address', 'level'];

    protected $hidden = ['data'];
}

最后是 Controller 实现 UsersController.php,里面演示了如何创建和修改:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;

class UsersController extends Controller
{
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function store()
    {
        $user = $this->user;

        $user->name = '老王';
        $user->address = '东北';
        $user->level = 1;
        $user->save();
    }

    public function update()
    {
        $user = $this->user->find(1);

        $user->address = '北京';
        $user->save();
    }
}

从代码演示中,我们可以看到,为了实现 Schemaless,虽然我们引入了虚拟字段的概念,但并没有对使用者造成困扰,基本用法没有任何变化,完全透明实现!

]]>
之所以要实现 Schemaless,主要是因为在线 DDL 有很多痛点,关于这一点,我在以前已经写过文章,没看过的不妨看看「史上最LOW的在线DDL解决方案」,不过那篇文章主要以介绍为主,并没有涉及具体的实现,所以我写了一个 Laravel 的例子。

首先创建测试用的 users 表,并且添加虚拟字段 name、address、level:

mysql> CREATE TABLE users (
           id INT UNSIGNED NOT NULL AUTO_INCREMENT,
           created_at timestamp null,
           updated_at timestamp null,
           data JSON NOT NULL,
           PRIMARY KEY(id)
       );

mysql> ALTER TABLE users add name VARCHAR(100) AS
       (JSON_UNQUOTE(JSON_EXTRACT(data, '$.name'))) AFTER id;

mysql> ALTER TABLE users add address VARCHAR(100) AS
       (JSON_UNQUOTE(JSON_EXTRACT(data, '$.address'))) AFTER name;

mysql> ALTER TABLE users add level INT UNSIGNED AS
       (JSON_EXTRACT(data, '$.level')) AFTER name;

然后是核心代码 Schemaless.php,以 trait 的方式实现:

<?php

namespace App;

trait Schemaless
{
    public function getDirty()
    {
        $dirty = collect(parent::getDirty());

        $keys = $dirty->keys()->map(function($key) {
            if (in_array($key, $this->virtual)) {
                $key = $this->getDataColumn() . '->' . $key;
            }

            return $key;
        });

        return $keys->combine($dirty)->all();
    }

    public function save(array $options = [])
    {
        if (!$this->exists) {
            $this->reviseRawAttributes();
        }

        return parent::save($options);
    }

    public function getDataColumn()
    {
        static $column;

        if ($column === null) {
            $column = defined('static::DATA') ? static::DATA : 'data';
        }

        return $column;
    }

    private function reviseRawAttributes()
    {
        $attributes = collect($this->getAttributes());

        $virtual = $attributes->only($this->virtual);

        $attributes = $attributes->diffKeys($virtual)->merge([
            $this->getDataColumn() => json_encode($virtual->all()),
        ]);

        $this->setRawAttributes($attributes->all());
    }
}

接着是 Model 实现 User.php,里面激活了 schemaless,并设置了虚拟字段:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use Schemaless;

    protected $virtual = ['name', 'address', 'level'];

    protected $hidden = ['data'];
}

最后是 Controller 实现 UsersController.php,里面演示了如何创建和修改:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;

class UsersController extends Controller
{
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function store()
    {
        $user = $this->user;

        $user->name = '老王';
        $user->address = '东北';
        $user->level = 1;
        $user->save();
    }

    public function update()
    {
        $user = $this->user->find(1);

        $user->address = '北京';
        $user->save();
    }
}

从代码演示中,我们可以看到,为了实现 Schemaless,虽然我们引入了虚拟字段的概念,但并没有对使用者造成困扰,基本用法没有任何变化,完全透明实现!

]]>
0
<![CDATA[小程序与老大哥]]> http://www.udpwork.com/item/16059.html http://www.udpwork.com/item/16059.html#reviews Thu, 12 Jan 2017 17:05:51 +0800 魏武挥 http://www.udpwork.com/item/16059.html

 

微信9日凌晨上线小程序,引起多方关注。有一位学者是这样评价的:

(前面有一些吧啦吧啦的场面话,就省了)微信是无意中崛起的,红包也是不经意间引爆的。具体产品层面我不多评说,拥有10亿级强粘性的用户的微信什么都有可能。但是,我要说说小程序的格局与价值观问题。格局肯定是没有的,价值观肯定是扭曲的。互联网精神肯定是违背的。充满了超级平台对权力的极致性追求,对控制用户的极限贪婪。充满了商业利益精明、精巧和精致的设计与布局。唯独看不到第三方和用户的权力和利益考量。用户无法积累自己的选择和偏好,第三方无法积累自己的用户,永远是平台的依附者和寄生者。一切的一切都在平台!这真是张小龙做的吗?看着小程序,我很狐疑,很诧异。必须让我重新思考张小龙、微信和腾讯的格局价值观,重估自己过去的认知和判断。总之,这次小程序不是张小龙神话的滑铁卢,就是我20年对互联网一向自信的判断力的滑铁卢,非此即彼。

我对这个评价是相当不以为然的,重点讲讲划线的那一部分话。

 

 

这位学者当年以“挑战微软霸权”一跃成名,甚至还因为系列文章被各大网站在微软的公关力量下撤稿而不忿,做了一个博客网站。后者在博客最红火的时候,乃是当时投资圈里的香饽饽。

时隔十余年,这位今天的南方某大学新传学院执行院长,依然没有跳出“挑战**霸权”的话语与框架。在他眼里,似乎所有公司巨头,都必须警惕,都有可能成为“老大哥”似的庞然怪物。

但遗憾的事是,没有什么商业公司能成为老大哥。

微软成为老大哥了吗?没有。谷歌成为老大哥了吗?并没有。苹果成为老大哥了吗?还是没有。Facebook成为老大哥了吗?依然没有。

如果说商业公司有点所谓的“权力”的话,那么,它的这个所谓“权力”,天然就是有笼子的。这个笼子就是:它的竞争者。

微软当年利用它庞大的广告预算,将这位学者的文章从几个主流门户上撤下,但后来的发展是:微软根本压不住批评者的声音。因为批评者自己也可以搞网站,搞公司,捣鼓出巨大的声响来。

人类近现代的历史证明,商业是伟大的,它带来了前所未见的进步和富裕。

但商业公司未必有多伟大,多高尚。

可商业公司也未必有多卑鄙,有多低劣。

一些科幻小说或影视作品,会想象出未来人类被一家公司所控制。这种想象,甚至还有一本很厚的书来做理论论述:当公司统治世界。

但恐怕,这都是想象。

在某些国家,商业公司有可能成为想做老大哥的人的工具——比较有名的事件是斯诺登事件(我加这么一句的原因可能你懂的)。但商业公司,很难成为老大哥。这也不是公司生来的使命。

在与真正的权力博弈的时候,再强大的商业公司,都未必就真的是强者,如果没有什么完善的制度的安排的话。

 

 

现在我们来具体说说微信的这个小程序。

这其实是相当冒险的一招。按照我一位朋友的说法,冒险就冒险在:张小龙试图将整个互联网生态里的“流量思维”改变为“价值思维”。

小程序并没有什么集中化的入口,它有可能被使用的场景比如是这样的:在饭店里吃饭,扫一个饭店里的二维码,得到这个饭店的小程序,可以点餐、可以买单。吃完擦擦嘴你就走了。这个饭店也不会因为你这次唤起过这个小程序,能天天向你推送什么。你也不是什么某饭店小程序的所谓“粉丝”。

小程序完成了一次价值的传递,但和流量,没什么关系。

小程序非常难推广,也不是什么流量集中化的产物。和苹果的AppStore比比就知道了。上不上AppStore排行榜,对一个App至关重要。

即便在谷歌的生态里,都有SEM(搜索引擎营销,采用技术化的手段将自己的网页在特定的搜索结果里排序靠前)的行为,在苹果的生态里,有刷榜的行为。因为谷歌和苹果,都是搞流量分发的。

我不知道小程序怎么就出现了“对权力的极致性追求,对控制用户的极限贪婪”。张小龙并没有搭建出一个小程序的集中分发平台,怎么就追求了一个“权力”,又怎么控制了用户。

你当百度和阿里,是吃干饭的么?

他们难道不会拼命游说饭店:嘿,你要部署小程序也可以,也部署部署我们的相关产品吧!

这是必然发生的事。

微信和张小龙,控制得了谁?!

 

 

小程序是微信一次极其大胆的尝试,因为它要和已经纵横了十余年的互联网流量思维相抗衡。

微信是有相当庞大的用户群体,但也不是做什么就必然能成什么。

这一次,你可以这样认为微信:自视过高不自量力,也可以那样认为微信:兵行险着前途未卜。

但你很难就在现在在这里大言不惭,将一顶“极致权力追求,极限控制贪婪”这样的老大哥也似的帽子,扔将过去。

看似义正辞严,其实,大而无当。

腾讯的目标的确是“连接一切”,但却不是“只有我连接一切”。

因为,它天然就有竞争者。

人家也不是吃干饭的。

 

 

说小程序的风险很大,原因在于:对于这样一个新生事物,参与者的利益驱动可能不足。

过去,商家们愿意铺设包括微信支付、支付宝支付等在内的支付手段,根子在于:它的费率比起银联略低。而且还有一个原因,大量的用户会问:你这里能不能微信/支付宝?如果不能,可能会失去一笔生意。

现在,对于线下商家来说,搭建一个小程序,好像利益并不够,而且,肯定有支出。

还是以前面提到的那个场景。当食客进门,并没有小程序,但有传统的点餐再加上微信支付,食客并不会觉得有太大的不便,饭店也不会因为没有小程序,丢失什么。

在有些场景里,可能参与者的利益驱动会比较大。比如我以前提到的硬件制造商用小程序来替代APP,从而使得它的消费者能用小程序操控所购买的硬件。

这个利益驱动就在于,开发一个小程序,比开发又要适用iOS又要适用Android的APP,成本低很多。而且基于web的特性,版本迭代也不需要用户再去主动做一次更新。

无论是饭店那个场景,还是硬件操控那个场景,我完全不知道,这位学者所谓“第三方无法积累自己的用户”指什么。卖硬件的,你硬件都卖出去了,用户间或就要唤起小程序来操控,怎么就没积累用户。

至于饭店,请问这位大学者,你跑一个小饭店里吃了一顿饭,这辈子都有可能不会再去了,你愿意作为饭店积累的用户?

 

 

本来这位学者这样评价一下微信,可能出于认识不足,或有可能出于思考不周密,倒也罢了。

偏偏这位学者陪同某有力人士遍游硅谷时,还大加赞许和褒扬。

讲真,政府官员有啥好褒扬的,他们所做的一切,都是他们应该做的。

所谓权力的极致追求,控制的极致贪婪,该落到谁的头上?

谁的格局在失去?

谁的价值观在扭曲?

学者做到这个份上,真心不觉得羞愧么?

 

—— 首发 扯氮集 ——

版权声明 与 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

小程序与老大哥,首发于扯氮集

]]>

 

微信9日凌晨上线小程序,引起多方关注。有一位学者是这样评价的:

(前面有一些吧啦吧啦的场面话,就省了)微信是无意中崛起的,红包也是不经意间引爆的。具体产品层面我不多评说,拥有10亿级强粘性的用户的微信什么都有可能。但是,我要说说小程序的格局与价值观问题。格局肯定是没有的,价值观肯定是扭曲的。互联网精神肯定是违背的。充满了超级平台对权力的极致性追求,对控制用户的极限贪婪。充满了商业利益精明、精巧和精致的设计与布局。唯独看不到第三方和用户的权力和利益考量。用户无法积累自己的选择和偏好,第三方无法积累自己的用户,永远是平台的依附者和寄生者。一切的一切都在平台!这真是张小龙做的吗?看着小程序,我很狐疑,很诧异。必须让我重新思考张小龙、微信和腾讯的格局价值观,重估自己过去的认知和判断。总之,这次小程序不是张小龙神话的滑铁卢,就是我20年对互联网一向自信的判断力的滑铁卢,非此即彼。

我对这个评价是相当不以为然的,重点讲讲划线的那一部分话。

 

 

这位学者当年以“挑战微软霸权”一跃成名,甚至还因为系列文章被各大网站在微软的公关力量下撤稿而不忿,做了一个博客网站。后者在博客最红火的时候,乃是当时投资圈里的香饽饽。

时隔十余年,这位今天的南方某大学新传学院执行院长,依然没有跳出“挑战**霸权”的话语与框架。在他眼里,似乎所有公司巨头,都必须警惕,都有可能成为“老大哥”似的庞然怪物。

但遗憾的事是,没有什么商业公司能成为老大哥。

微软成为老大哥了吗?没有。谷歌成为老大哥了吗?并没有。苹果成为老大哥了吗?还是没有。Facebook成为老大哥了吗?依然没有。

如果说商业公司有点所谓的“权力”的话,那么,它的这个所谓“权力”,天然就是有笼子的。这个笼子就是:它的竞争者。

微软当年利用它庞大的广告预算,将这位学者的文章从几个主流门户上撤下,但后来的发展是:微软根本压不住批评者的声音。因为批评者自己也可以搞网站,搞公司,捣鼓出巨大的声响来。

人类近现代的历史证明,商业是伟大的,它带来了前所未见的进步和富裕。

但商业公司未必有多伟大,多高尚。

可商业公司也未必有多卑鄙,有多低劣。

一些科幻小说或影视作品,会想象出未来人类被一家公司所控制。这种想象,甚至还有一本很厚的书来做理论论述:当公司统治世界。

但恐怕,这都是想象。

在某些国家,商业公司有可能成为想做老大哥的人的工具——比较有名的事件是斯诺登事件(我加这么一句的原因可能你懂的)。但商业公司,很难成为老大哥。这也不是公司生来的使命。

在与真正的权力博弈的时候,再强大的商业公司,都未必就真的是强者,如果没有什么完善的制度的安排的话。

 

 

现在我们来具体说说微信的这个小程序。

这其实是相当冒险的一招。按照我一位朋友的说法,冒险就冒险在:张小龙试图将整个互联网生态里的“流量思维”改变为“价值思维”。

小程序并没有什么集中化的入口,它有可能被使用的场景比如是这样的:在饭店里吃饭,扫一个饭店里的二维码,得到这个饭店的小程序,可以点餐、可以买单。吃完擦擦嘴你就走了。这个饭店也不会因为你这次唤起过这个小程序,能天天向你推送什么。你也不是什么某饭店小程序的所谓“粉丝”。

小程序完成了一次价值的传递,但和流量,没什么关系。

小程序非常难推广,也不是什么流量集中化的产物。和苹果的AppStore比比就知道了。上不上AppStore排行榜,对一个App至关重要。

即便在谷歌的生态里,都有SEM(搜索引擎营销,采用技术化的手段将自己的网页在特定的搜索结果里排序靠前)的行为,在苹果的生态里,有刷榜的行为。因为谷歌和苹果,都是搞流量分发的。

我不知道小程序怎么就出现了“对权力的极致性追求,对控制用户的极限贪婪”。张小龙并没有搭建出一个小程序的集中分发平台,怎么就追求了一个“权力”,又怎么控制了用户。

你当百度和阿里,是吃干饭的么?

他们难道不会拼命游说饭店:嘿,你要部署小程序也可以,也部署部署我们的相关产品吧!

这是必然发生的事。

微信和张小龙,控制得了谁?!

 

 

小程序是微信一次极其大胆的尝试,因为它要和已经纵横了十余年的互联网流量思维相抗衡。

微信是有相当庞大的用户群体,但也不是做什么就必然能成什么。

这一次,你可以这样认为微信:自视过高不自量力,也可以那样认为微信:兵行险着前途未卜。

但你很难就在现在在这里大言不惭,将一顶“极致权力追求,极限控制贪婪”这样的老大哥也似的帽子,扔将过去。

看似义正辞严,其实,大而无当。

腾讯的目标的确是“连接一切”,但却不是“只有我连接一切”。

因为,它天然就有竞争者。

人家也不是吃干饭的。

 

 

说小程序的风险很大,原因在于:对于这样一个新生事物,参与者的利益驱动可能不足。

过去,商家们愿意铺设包括微信支付、支付宝支付等在内的支付手段,根子在于:它的费率比起银联略低。而且还有一个原因,大量的用户会问:你这里能不能微信/支付宝?如果不能,可能会失去一笔生意。

现在,对于线下商家来说,搭建一个小程序,好像利益并不够,而且,肯定有支出。

还是以前面提到的那个场景。当食客进门,并没有小程序,但有传统的点餐再加上微信支付,食客并不会觉得有太大的不便,饭店也不会因为没有小程序,丢失什么。

在有些场景里,可能参与者的利益驱动会比较大。比如我以前提到的硬件制造商用小程序来替代APP,从而使得它的消费者能用小程序操控所购买的硬件。

这个利益驱动就在于,开发一个小程序,比开发又要适用iOS又要适用Android的APP,成本低很多。而且基于web的特性,版本迭代也不需要用户再去主动做一次更新。

无论是饭店那个场景,还是硬件操控那个场景,我完全不知道,这位学者所谓“第三方无法积累自己的用户”指什么。卖硬件的,你硬件都卖出去了,用户间或就要唤起小程序来操控,怎么就没积累用户。

至于饭店,请问这位大学者,你跑一个小饭店里吃了一顿饭,这辈子都有可能不会再去了,你愿意作为饭店积累的用户?

 

 

本来这位学者这样评价一下微信,可能出于认识不足,或有可能出于思考不周密,倒也罢了。

偏偏这位学者陪同某有力人士遍游硅谷时,还大加赞许和褒扬。

讲真,政府官员有啥好褒扬的,他们所做的一切,都是他们应该做的。

所谓权力的极致追求,控制的极致贪婪,该落到谁的头上?

谁的格局在失去?

谁的价值观在扭曲?

学者做到这个份上,真心不觉得羞愧么?

 

—— 首发 扯氮集 ——

版权声明 与 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

小程序与老大哥,首发于扯氮集

]]>
0
<![CDATA[小程序才是微信的初心]]> http://www.udpwork.com/item/16060.html http://www.udpwork.com/item/16060.html#reviews Thu, 12 Jan 2017 17:03:57 +0800 魏武挥 http://www.udpwork.com/item/16060.html

 

先把时间推回到2013年6月(并不是2007年1月9日),距离微信推出公众平台,大概是8个月的样子。

在这八个月中,微信从来没公开宣讲过什么。低调,神秘,一如张小龙本人的风格。

这一天,时任微信产品总监的曾鸣,来到了腾讯把脉沙龙,第一次对外宣讲微信公众平台。同时,还带来了几个精品案例——这应该就是微信公开课的前身。

这几个案例分别是:南方航空、招商银行、广州公安、国家博物馆、广东联通、央视新闻和商业价值。

最后两个属于媒体。

有趣的是,曾鸣在宣讲中,提到了这样两句话:

微信数据显示,你发的内容越猛,用户掉得越多。因为用户已经被骚扰过度。

所以我们要说的一句话是,微信不是营销工具。微信可能会侧重服务化和精品化的路线。

这句话更关键:

微信公众号不在于大小,而在于价值。哪怕我一个月甚至是半年才用一次,只要那次用的时候,你提供的东西有价值,我就不会从通讯录里删除你,你至少会把我黏住。

当时非常有名的一个自媒体人潘越飞在这场宣讲会后,写下了这样一段听后感言:

自媒体还不是微信公众平台的重心。

 

 

其实做公号内容久的人,都应该有这种印象:对媒体两个字,微信一开始是不待见的。

一直到今天,微信公号依然有让一些搞内容的人不怎么舒服的地方,比方说,大部分公号,一天只能推送一次。推送完了,发现个错别字,还不让改。

如果说拒绝承认自己是媒体的张一鸣心里还是明白今日头条是靠内容吃饭的话,我想,张小龙打一开始,就没料到,微信公号平台,居然成了中国时下最大的内容生态,真没有之一。

依靠微信蓬勃而起的腾讯效果广告,包括朋友圈广告、广点通、互选广告等,其实和微信事业部并没有什么关系。虽然张小龙在广告上很有话语权,但业绩真的不算WXG。

如果你有兴趣的话,可以到网上搜索一下13年那场腾讯把脉沙龙里的文字记录。你会发现,南方航空、招商银行、广州公安、国家博物馆、广东联通这几个当年的微信公号精品案例,竟然在今天,几乎都可以用小程序来解决!

你再琢磨琢磨曾鸣的这句话:

微信公众号不在于大小,而在于价值。哪怕我一个月甚至是半年才用一次,只要那次用的时候,你提供的东西有价值,我就不会从通讯录里删除你,你至少会把我黏住。

把公众号三个字替换成小程序,是不是更加妥贴,更加合拍?

笑。

 

 

中国移动互联网走到今天,最大的一个误会就是,公号居然成了内容创业的原点,居然成就了一大把所谓的新媒体。

我想张小龙对于这个事实,恐怕是有些哭笑不得的。

媒体有两个核心关键词,其一订户,其二推送,而公号恰好都具备。但我不觉得微信是有意为之。它只是延续了当年博客+RSS的一种体例罢了。

公号甚至还推出了一个重要的数据:十万加。

我一直认为,十万加是内容创业兴起的一个tipping point——不是说是重要成因,而是说引爆了这个领域。

当提供出一个外界可以观测的指标后,公号规模化的营销业务洽谈,投融资洽谈,成为一种可能。

但公号其实一开始对这个指标的推出,是拒绝的。

新榜创始人徐达内清楚地记得,2014年7月24日,微信推出公号阅读计数——这是他今天成为一只人民币独角兽的基石。

而这个时间点,自媒体大潮已然兴起。之后,内容创业事实上呼啸而来,虽然一直到2016年年头,新榜才推出“内容创业之春”这样的说法。

我不禁想起了这样一句话:技术发明人会失去对他发明的这个技术的未来使用的控制。

诚如斯言。

 

 

其实微信一直在努力去实现它的这份初心。

一个很明显的动作就是把公号一分为二,内容类的,叫订阅号,并加以折叠。另外一部分,称为服务号。服务号享受包括不被折叠在内的一些特权,但它只能一周推送一次。

微信期待服务号能够把微信的生态扭转到它的初心上来,但不得不说,结果差强人意。

服务号依然是一个公号体系,它具备公号的几个基础特征:订阅数、访问量,以及,推送。

在大的框架已定的情况下,服务号想要实现微信的初心,极其困难。

索性推倒重来,把既定的大框架都推翻。

小程序就这样诞生了。

一开始小程序叫应用号,有可能因为苹果的敏感,改名为小程序。但这未尝不是一个好事。依然叫什么什么号,始终有一种和订阅号、服务号、企业号的大框架一致的感觉。

如果说,微信公号派生出这个号那个号,只是12年10月推出公众平台的一种延续的话,

小程序,基本上可以认为,这是一次“再出发”。

 

 

张小龙的个人历史,使得他始终和营销这两个字若即若离。

张小龙是做邮件出身的,早年开发了一款当时相当火的邮件客户端Foxmail。后来进入腾讯,承担QQ邮箱。

邮件本身的技术并不困难,我以前在一家证券公司找了个技术人员,两三天就搭出了一个邮箱系统。邮件当年最困难的地方是和垃圾邮件角力:如何保证用户看到的邮件都是ta真的想看到的,且,ta想看到的都能全部给ta看到。

垃圾邮件就是一种非常古老的互联网营销行为,时至今日,依然有人在使用。

而营销,天然和内容联系在一起。

今天的内容创业项目,9成以上,吃的都是营销饭——有的是主吃,有的是配菜。一点不吃,极其稀少。

小程序没有推送,不能分享到朋友圈,二维码仅供手机摄像头扫码识别,林林总总,都在尽可能地最大化地拒绝被当成营销工具。

回忆一下曾鸣在13年的那句话吧:

微信,不是一个营销工具。

只是后来微信实在扛不过大势,只好羞羞答答地改为这样的说法:我们反对恶意营销。并为公号设定这样的规矩:但凡引诱关注、引诱分享的,都叫恶意营销,需要处罚。

请各位搞营销的告诉我,这个时代什么样的营销,不是尽可能让你关注,尽可能让你分享?

 

 

9日凌晨,小程序发布。

媒体人,或者说,内容创业者,天然都是相当敏感的。

到目前所能见到的很多小程序,不乏是搞内容的在试水。国家队甚至也跳了进来。

公号已经让诸多媒体削足适履地去适应微信的法则。

这一次,会不会让内容创业者们,再一次削足适履?

也许,可以复制一次成功和辉煌,比如着力在社群运营。

也许,折腾了半天,才发现,小程序真心不是自己的菜。

能让张小龙再一次哭笑不得吗?

还是得走着瞧。

 

—— 首发 新榜 ——

 

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

小程序才是微信的初心,首发于扯氮集

]]>

 

先把时间推回到2013年6月(并不是2007年1月9日),距离微信推出公众平台,大概是8个月的样子。

在这八个月中,微信从来没公开宣讲过什么。低调,神秘,一如张小龙本人的风格。

这一天,时任微信产品总监的曾鸣,来到了腾讯把脉沙龙,第一次对外宣讲微信公众平台。同时,还带来了几个精品案例——这应该就是微信公开课的前身。

这几个案例分别是:南方航空、招商银行、广州公安、国家博物馆、广东联通、央视新闻和商业价值。

最后两个属于媒体。

有趣的是,曾鸣在宣讲中,提到了这样两句话:

微信数据显示,你发的内容越猛,用户掉得越多。因为用户已经被骚扰过度。

所以我们要说的一句话是,微信不是营销工具。微信可能会侧重服务化和精品化的路线。

这句话更关键:

微信公众号不在于大小,而在于价值。哪怕我一个月甚至是半年才用一次,只要那次用的时候,你提供的东西有价值,我就不会从通讯录里删除你,你至少会把我黏住。

当时非常有名的一个自媒体人潘越飞在这场宣讲会后,写下了这样一段听后感言:

自媒体还不是微信公众平台的重心。

 

 

其实做公号内容久的人,都应该有这种印象:对媒体两个字,微信一开始是不待见的。

一直到今天,微信公号依然有让一些搞内容的人不怎么舒服的地方,比方说,大部分公号,一天只能推送一次。推送完了,发现个错别字,还不让改。

如果说拒绝承认自己是媒体的张一鸣心里还是明白今日头条是靠内容吃饭的话,我想,张小龙打一开始,就没料到,微信公号平台,居然成了中国时下最大的内容生态,真没有之一。

依靠微信蓬勃而起的腾讯效果广告,包括朋友圈广告、广点通、互选广告等,其实和微信事业部并没有什么关系。虽然张小龙在广告上很有话语权,但业绩真的不算WXG。

如果你有兴趣的话,可以到网上搜索一下13年那场腾讯把脉沙龙里的文字记录。你会发现,南方航空、招商银行、广州公安、国家博物馆、广东联通这几个当年的微信公号精品案例,竟然在今天,几乎都可以用小程序来解决!

你再琢磨琢磨曾鸣的这句话:

微信公众号不在于大小,而在于价值。哪怕我一个月甚至是半年才用一次,只要那次用的时候,你提供的东西有价值,我就不会从通讯录里删除你,你至少会把我黏住。

把公众号三个字替换成小程序,是不是更加妥贴,更加合拍?

笑。

 

 

中国移动互联网走到今天,最大的一个误会就是,公号居然成了内容创业的原点,居然成就了一大把所谓的新媒体。

我想张小龙对于这个事实,恐怕是有些哭笑不得的。

媒体有两个核心关键词,其一订户,其二推送,而公号恰好都具备。但我不觉得微信是有意为之。它只是延续了当年博客+RSS的一种体例罢了。

公号甚至还推出了一个重要的数据:十万加。

我一直认为,十万加是内容创业兴起的一个tipping point——不是说是重要成因,而是说引爆了这个领域。

当提供出一个外界可以观测的指标后,公号规模化的营销业务洽谈,投融资洽谈,成为一种可能。

但公号其实一开始对这个指标的推出,是拒绝的。

新榜创始人徐达内清楚地记得,2014年7月24日,微信推出公号阅读计数——这是他今天成为一只人民币独角兽的基石。

而这个时间点,自媒体大潮已然兴起。之后,内容创业事实上呼啸而来,虽然一直到2016年年头,新榜才推出“内容创业之春”这样的说法。

我不禁想起了这样一句话:技术发明人会失去对他发明的这个技术的未来使用的控制。

诚如斯言。

 

 

其实微信一直在努力去实现它的这份初心。

一个很明显的动作就是把公号一分为二,内容类的,叫订阅号,并加以折叠。另外一部分,称为服务号。服务号享受包括不被折叠在内的一些特权,但它只能一周推送一次。

微信期待服务号能够把微信的生态扭转到它的初心上来,但不得不说,结果差强人意。

服务号依然是一个公号体系,它具备公号的几个基础特征:订阅数、访问量,以及,推送。

在大的框架已定的情况下,服务号想要实现微信的初心,极其困难。

索性推倒重来,把既定的大框架都推翻。

小程序就这样诞生了。

一开始小程序叫应用号,有可能因为苹果的敏感,改名为小程序。但这未尝不是一个好事。依然叫什么什么号,始终有一种和订阅号、服务号、企业号的大框架一致的感觉。

如果说,微信公号派生出这个号那个号,只是12年10月推出公众平台的一种延续的话,

小程序,基本上可以认为,这是一次“再出发”。

 

 

张小龙的个人历史,使得他始终和营销这两个字若即若离。

张小龙是做邮件出身的,早年开发了一款当时相当火的邮件客户端Foxmail。后来进入腾讯,承担QQ邮箱。

邮件本身的技术并不困难,我以前在一家证券公司找了个技术人员,两三天就搭出了一个邮箱系统。邮件当年最困难的地方是和垃圾邮件角力:如何保证用户看到的邮件都是ta真的想看到的,且,ta想看到的都能全部给ta看到。

垃圾邮件就是一种非常古老的互联网营销行为,时至今日,依然有人在使用。

而营销,天然和内容联系在一起。

今天的内容创业项目,9成以上,吃的都是营销饭——有的是主吃,有的是配菜。一点不吃,极其稀少。

小程序没有推送,不能分享到朋友圈,二维码仅供手机摄像头扫码识别,林林总总,都在尽可能地最大化地拒绝被当成营销工具。

回忆一下曾鸣在13年的那句话吧:

微信,不是一个营销工具。

只是后来微信实在扛不过大势,只好羞羞答答地改为这样的说法:我们反对恶意营销。并为公号设定这样的规矩:但凡引诱关注、引诱分享的,都叫恶意营销,需要处罚。

请各位搞营销的告诉我,这个时代什么样的营销,不是尽可能让你关注,尽可能让你分享?

 

 

9日凌晨,小程序发布。

媒体人,或者说,内容创业者,天然都是相当敏感的。

到目前所能见到的很多小程序,不乏是搞内容的在试水。国家队甚至也跳了进来。

公号已经让诸多媒体削足适履地去适应微信的法则。

这一次,会不会让内容创业者们,再一次削足适履?

也许,可以复制一次成功和辉煌,比如着力在社群运营。

也许,折腾了半天,才发现,小程序真心不是自己的菜。

能让张小龙再一次哭笑不得吗?

还是得走着瞧。

 

—— 首发 新榜 ——

 

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

小程序才是微信的初心,首发于扯氮集

]]>
0
<![CDATA[“我的脑袋里没有克制两个字”]]> http://www.udpwork.com/item/16058.html http://www.udpwork.com/item/16058.html#reviews Wed, 11 Jan 2017 00:30:44 +0800 魏武挥 http://www.udpwork.com/item/16058.html

去年12月28日的广州微信公开课pro,张小龙一个半小时的演讲中,这句话给我留下了极为深刻的印象:

我的脑袋里没有克制两个字。我只是觉得,这个事不应该做或者做不到,所以我不做。这并不是克制。

小程序虽然叫“小”程序,但绝不是一个小小的功能或变化。

事实上,小程序一开始的名字叫“应用号”,极有可能与苹果的有关政策冲突而改名为“小程序”。苹果依靠AppStore(应用商店)一手掌控了苹果生态内应用的分发与变现,对应用两个字是极为敏感的。

2017年1月9日,小程序正式上线。

而十年前的今天,iPhone发布。

不知道是不是张小龙对乔布斯的一次致敬。

 

移动互联网开始后,从工具使用上,大致上经历了两段创业波:APP创业,公号创业。

这就是为什么乔布斯和张小龙得以封神的原因。

乔布斯掌控了iOS生态里APP的分发与变现。

张小龙掌控了微信生态里内容的分发与部分变现。

比尔盖茨也是被封过神的,因为的确存在大量的“软件创业”,并依托于windows得以存在及发展壮大。但比尔盖茨并没有掌控软件的分发与变现,他只是提供了软件运行的基础环境。相较之下,还弱上一点。

而事实上,马云在一个特定的领域里,也是近乎神一般的存在。一直到今天,你都可以在很多机场书店里看到马云的视频,前面还有不少人驻足观看。

马云促发的事是:电商创业。

一个能让自己快速致富成为一方巨头的生意,可以称为Big Idea。

而一个能让极多的人致富其中间或还能冒出来一些巨头的生意,可以称为Great Idea。

小程序能不能成为Great Idea,承载张小龙“我的脑袋里没有克制两个字”的野心?

 

任何一盘生意,在今天这个时代,非常讲究“客户留存”。

过去,一批传统企业在卖货之时,很容易忽视这个问题——有些则是因为依靠代理、渠道这种第三方进行产品销售而无法获取用户资料,走到现在,头疼无比,因为他们只有销量,没有留存的用户。

小程序是一种无法沉淀出用户的工具,它压根没有订阅机制,与APP的所谓装机量,公号的所谓粉丝完全不同。缺少这个指标,在当下的创投圈内,很难向投资人说出一个什么BP或故事。

从用户的角度,用完即走是非常没有负担的,也是乐意的。

但从企业角度,这真的是一场噩梦。

很少有企业能够像谷歌那样,让用户以最快的速度得到搜索结果,然后离开。9成9以上的企业,还是希望用户能再待一点时间。

所以互联网行业里,有非常重要的一个考量指标:用户时长。

所以,要像APP创业、公号创业那样,以小程序为原点进行创业,这是非常难以想象的。

小程序只能作为提高某种生意效率的工具,而不是生意本身。

 

马化腾曾经提出过“连接一切”,也提出过“二维码是入口”这样的话语。

小程序是用来承担这样的使命的:将线下包括服务业在内的各种生意,用小程序,连接到线上。

所以,小程序主要是给O2O用的,它的二维码只能用手机摄像头扫码识别。

想象一下这样一个场景:

你进入了一家饭店,扫码了饭桌上的一个二维码,得到一个小程序。这个小程序能够完成点餐功能,还能完成买单结算功能。用完即走,银货两讫。

在这个场景中,你没有负担,让你就是吃顿饭就要下一个饭店APP,实在过于夸张。订阅这家饭店的公号,恐怕也是吃完就退订。

饭店能节省很多成本,比如菜单就是一个不菲的成本,甚至可以减少一点店堂里的服务人员,因为买单结算是自助的。

小程序就是用来解决这样的必需的但又不是高频的服务。

如果你真的那么喜欢那家饭店,每餐都在那里吃,还是建议你下载它的APP算了。

一些智能硬件制造公司也可以使用到小程序。

比如很多空气净化器、蓝牙音箱都提供APP,让用户得以用手机来控制设备。未来可以用小程序来实现APP的功能。

对于硬件制造商而言,APP的装机量本来就不是什么核心指标,关键是硬件的销售。

 

虽然小程序原名叫应用号,但要说小程序能彻底取代今天的APP,恐怕有点理想化。

小程序目前的容量只有1M,这是一个极小的容量,很难做出特别复杂的功能——当然,未来有可能会放大到2M、5M,随着中国移动互联网的网速加快,程序容量本身不是什么大问题。

真正的原因在这里:其实中国移动用户对APP的装机并不那么热情。TalkingData的数据显示,中国平均装APP的数量是:30-35个,这里还包括手机的预装应用,比如苹果的预装,就不下十个。

移动互联网在中国发展近十年,这样的APP装机水平说明,APP的巨岛效应已经形成。而让这些已经坐拥千万当量级用户的互联网公司放弃APP,投向小程序的怀抱,商业上是很难想象的。

16G的iPhone手机用户可能会尽可能地减少APP的下载和装机,但对于更多手机用户而言,弃常用APP而改用微信的小程序,习惯上的改变,也很艰难。

 

机缘巧合,曾经和微信团队中的一个重要成员吃饭。

在他看来,微信公号始终是一个相当复杂的东西,这意味着这个功能只是给一小撮人用的。

事实的确如此,公号数量号称两千万,比起七八亿微信用户,很显然是小众群体在使用。

微信似乎一直想让非TMT圈的人也能再供给点什么,如同朋友圈、微信红包。

对于小程序来说,微信的野心绝对不是一小撮人在利用小程序,换句话说,小程序并不希望只有IT圈、媒体圈在使用,虽然现下第一波尝鲜者大多是这两个圈子里的。

取代APP生态,让所有现在做APP的人都改行开发小程序,并不是微信的目标。

小程序也不太会成为一个创业的原点,小程序创业可能性非常小(除了那些专职为企业制作小程序的公司,类似好多年前的建站公司,这个方向可能真是一个能挖到金子的所在)。

小程序是用来提升既有商业效率的。这话的意思就是:你已经有一盘生意,比如一个饭店,或者一个硬件制造。

如果把这些都连接起来,那么

你觉得它是一个Big Idea呢,还是一个Great Idea?

 

—— 首发 上海观察 ——

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

“我的脑袋里没有克制两个字”,首发于扯氮集

]]>

去年12月28日的广州微信公开课pro,张小龙一个半小时的演讲中,这句话给我留下了极为深刻的印象:

我的脑袋里没有克制两个字。我只是觉得,这个事不应该做或者做不到,所以我不做。这并不是克制。

小程序虽然叫“小”程序,但绝不是一个小小的功能或变化。

事实上,小程序一开始的名字叫“应用号”,极有可能与苹果的有关政策冲突而改名为“小程序”。苹果依靠AppStore(应用商店)一手掌控了苹果生态内应用的分发与变现,对应用两个字是极为敏感的。

2017年1月9日,小程序正式上线。

而十年前的今天,iPhone发布。

不知道是不是张小龙对乔布斯的一次致敬。

 

移动互联网开始后,从工具使用上,大致上经历了两段创业波:APP创业,公号创业。

这就是为什么乔布斯和张小龙得以封神的原因。

乔布斯掌控了iOS生态里APP的分发与变现。

张小龙掌控了微信生态里内容的分发与部分变现。

比尔盖茨也是被封过神的,因为的确存在大量的“软件创业”,并依托于windows得以存在及发展壮大。但比尔盖茨并没有掌控软件的分发与变现,他只是提供了软件运行的基础环境。相较之下,还弱上一点。

而事实上,马云在一个特定的领域里,也是近乎神一般的存在。一直到今天,你都可以在很多机场书店里看到马云的视频,前面还有不少人驻足观看。

马云促发的事是:电商创业。

一个能让自己快速致富成为一方巨头的生意,可以称为Big Idea。

而一个能让极多的人致富其中间或还能冒出来一些巨头的生意,可以称为Great Idea。

小程序能不能成为Great Idea,承载张小龙“我的脑袋里没有克制两个字”的野心?

 

任何一盘生意,在今天这个时代,非常讲究“客户留存”。

过去,一批传统企业在卖货之时,很容易忽视这个问题——有些则是因为依靠代理、渠道这种第三方进行产品销售而无法获取用户资料,走到现在,头疼无比,因为他们只有销量,没有留存的用户。

小程序是一种无法沉淀出用户的工具,它压根没有订阅机制,与APP的所谓装机量,公号的所谓粉丝完全不同。缺少这个指标,在当下的创投圈内,很难向投资人说出一个什么BP或故事。

从用户的角度,用完即走是非常没有负担的,也是乐意的。

但从企业角度,这真的是一场噩梦。

很少有企业能够像谷歌那样,让用户以最快的速度得到搜索结果,然后离开。9成9以上的企业,还是希望用户能再待一点时间。

所以互联网行业里,有非常重要的一个考量指标:用户时长。

所以,要像APP创业、公号创业那样,以小程序为原点进行创业,这是非常难以想象的。

小程序只能作为提高某种生意效率的工具,而不是生意本身。

 

马化腾曾经提出过“连接一切”,也提出过“二维码是入口”这样的话语。

小程序是用来承担这样的使命的:将线下包括服务业在内的各种生意,用小程序,连接到线上。

所以,小程序主要是给O2O用的,它的二维码只能用手机摄像头扫码识别。

想象一下这样一个场景:

你进入了一家饭店,扫码了饭桌上的一个二维码,得到一个小程序。这个小程序能够完成点餐功能,还能完成买单结算功能。用完即走,银货两讫。

在这个场景中,你没有负担,让你就是吃顿饭就要下一个饭店APP,实在过于夸张。订阅这家饭店的公号,恐怕也是吃完就退订。

饭店能节省很多成本,比如菜单就是一个不菲的成本,甚至可以减少一点店堂里的服务人员,因为买单结算是自助的。

小程序就是用来解决这样的必需的但又不是高频的服务。

如果你真的那么喜欢那家饭店,每餐都在那里吃,还是建议你下载它的APP算了。

一些智能硬件制造公司也可以使用到小程序。

比如很多空气净化器、蓝牙音箱都提供APP,让用户得以用手机来控制设备。未来可以用小程序来实现APP的功能。

对于硬件制造商而言,APP的装机量本来就不是什么核心指标,关键是硬件的销售。

 

虽然小程序原名叫应用号,但要说小程序能彻底取代今天的APP,恐怕有点理想化。

小程序目前的容量只有1M,这是一个极小的容量,很难做出特别复杂的功能——当然,未来有可能会放大到2M、5M,随着中国移动互联网的网速加快,程序容量本身不是什么大问题。

真正的原因在这里:其实中国移动用户对APP的装机并不那么热情。TalkingData的数据显示,中国平均装APP的数量是:30-35个,这里还包括手机的预装应用,比如苹果的预装,就不下十个。

移动互联网在中国发展近十年,这样的APP装机水平说明,APP的巨岛效应已经形成。而让这些已经坐拥千万当量级用户的互联网公司放弃APP,投向小程序的怀抱,商业上是很难想象的。

16G的iPhone手机用户可能会尽可能地减少APP的下载和装机,但对于更多手机用户而言,弃常用APP而改用微信的小程序,习惯上的改变,也很艰难。

 

机缘巧合,曾经和微信团队中的一个重要成员吃饭。

在他看来,微信公号始终是一个相当复杂的东西,这意味着这个功能只是给一小撮人用的。

事实的确如此,公号数量号称两千万,比起七八亿微信用户,很显然是小众群体在使用。

微信似乎一直想让非TMT圈的人也能再供给点什么,如同朋友圈、微信红包。

对于小程序来说,微信的野心绝对不是一小撮人在利用小程序,换句话说,小程序并不希望只有IT圈、媒体圈在使用,虽然现下第一波尝鲜者大多是这两个圈子里的。

取代APP生态,让所有现在做APP的人都改行开发小程序,并不是微信的目标。

小程序也不太会成为一个创业的原点,小程序创业可能性非常小(除了那些专职为企业制作小程序的公司,类似好多年前的建站公司,这个方向可能真是一个能挖到金子的所在)。

小程序是用来提升既有商业效率的。这话的意思就是:你已经有一盘生意,比如一个饭店,或者一个硬件制造。

如果把这些都连接起来,那么

你觉得它是一个Big Idea呢,还是一个Great Idea?

 

—— 首发 上海观察 ——

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

“我的脑袋里没有克制两个字”,首发于扯氮集

]]>
0
<![CDATA[一个事半功倍的Java反射库]]> http://www.udpwork.com/item/16057.html http://www.udpwork.com/item/16057.html#reviews Mon, 09 Jan 2017 19:02:00 +0800 技术小黑屋 http://www.udpwork.com/item/16057.html 在Java和Android中,我们常常会使用反射来达到一些兼容的目的。Java原生提供的反射很是麻烦,使用起来很是不方便。比如我们想要调UserManager的静态方法get,使用原生的实现如下

1
2
3
4
5
6
7
8
9
10
11
try {
    final Method m = UserManager.class.getMethod("get", Context.class);
    m.setAccessible(true);
    m.invoke(null, this);
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}

实现起来好不麻烦。这其中

  • 需要确定方法名和参数来获取对应的Method对象
  • 设置Method对象的assessible为true
  • 调用invoke方法,并且传入对应的参数
  • 捕获其中可能抛出来的一连串异常

那么反射能简单点么,当然,而且还会简单很多。

这就是本文想要介绍的,jOOR(Java Object Oriented Reflection),它是一个对java.lang.reflect包的简单封装,使得我们使用起来更加直接和方便。

使用jOOR,上面的代码可以缩短成一行。

1
Reflect.on(UserManager.class).call("get", getApplicationContext());

依赖

API介绍

Reflect

  • Reflect.on 包裹一个类或者对象,表示在这个类或对象上进行反射,类的值可以使Class,也可以是完整的类名(包含包名信息)
  • Reflect.create 用来调用之前的类的构造方法,有两种重载,一种有参数,一种无参数
  • Reflect.call 方法调用,传入方法名和参数,如有返回值还需要调用get
  • Reflect.get 获取(field和method返回)值相关,会进行类型转换,常与call和field组合使用
  • Reflect.field 获取属性值相关,需要调用get获取该值
  • Reflect.set 设置属性相关。

ReflectException

引入ReflectException避免了我们去catch过多的异常,也减少了纵向代码量,使得代码简洁不少。ReflectException抛出,可能是发生了以下异常。

  • ClassNotFoundException
  • IllegalAccessException
  • IllegalArgumentException
  • InstantiationException
  • InvocationTargetException
  • NoSuchMethodException
  • NoSuchFieldException
  • SecurityException

除此之外,ReflectException属于unchecked 异常,语法上不需要显式进行捕获,但是也需要根据实际情况,斟酌是否进行显式捕获该异常。

使用示例

创建实例

1
String string = Reflect.on(String.class).create("Hello World").get();

访问属性(public,protected,package,private均可)

1
char pathSeparatorChar = Reflect.on(File.class).create("/sdcard/droidyue.com").field("pathSeparatorChar").get();

修改属性(final属性也可以修改)

1
String setValue = Reflect.on(File.class).create("/sdcard/drodiyue.com").set("path", "fakepath").get("path");

调用方法(public,protected,package,private均可)

1
2
3
4
ArrayList arrayList = new ArrayList();
arrayList.add("Hello");
arrayList.add("World");
int value = Reflect.on(arrayList).call("hugeCapacity", 12).get();

实现原理

Reflect实际是对原生java reflect进行封装,屏蔽了无关细节。

以fields方法为例,其内部实现可以看出是调用了java原生提供的反射相关的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Map<String, Reflect> fields() {
    Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
    Class<?> type = type();

    do {
        for (Field field : type.getDeclaredFields()) {
            if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
                String name = field.getName();

            if (!result.containsKey(name))
              result.put(name, field(name));
            }
        }

        type = type.getSuperclass();
    } while (type != null);

    return result;
}

库地址

以上就是这些,希望jOOR可以对大家的开发日常有所帮助。

]]>
在Java和Android中,我们常常会使用反射来达到一些兼容的目的。Java原生提供的反射很是麻烦,使用起来很是不方便。比如我们想要调UserManager的静态方法get,使用原生的实现如下

1
2
3
4
5
6
7
8
9
10
11
try {
    final Method m = UserManager.class.getMethod("get", Context.class);
    m.setAccessible(true);
    m.invoke(null, this);
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}

实现起来好不麻烦。这其中

  • 需要确定方法名和参数来获取对应的Method对象
  • 设置Method对象的assessible为true
  • 调用invoke方法,并且传入对应的参数
  • 捕获其中可能抛出来的一连串异常

那么反射能简单点么,当然,而且还会简单很多。

这就是本文想要介绍的,jOOR(Java Object Oriented Reflection),它是一个对java.lang.reflect包的简单封装,使得我们使用起来更加直接和方便。

使用jOOR,上面的代码可以缩短成一行。

1
Reflect.on(UserManager.class).call("get", getApplicationContext());

依赖

API介绍

Reflect

  • Reflect.on 包裹一个类或者对象,表示在这个类或对象上进行反射,类的值可以使Class,也可以是完整的类名(包含包名信息)
  • Reflect.create 用来调用之前的类的构造方法,有两种重载,一种有参数,一种无参数
  • Reflect.call 方法调用,传入方法名和参数,如有返回值还需要调用get
  • Reflect.get 获取(field和method返回)值相关,会进行类型转换,常与call和field组合使用
  • Reflect.field 获取属性值相关,需要调用get获取该值
  • Reflect.set 设置属性相关。

ReflectException

引入ReflectException避免了我们去catch过多的异常,也减少了纵向代码量,使得代码简洁不少。ReflectException抛出,可能是发生了以下异常。

  • ClassNotFoundException
  • IllegalAccessException
  • IllegalArgumentException
  • InstantiationException
  • InvocationTargetException
  • NoSuchMethodException
  • NoSuchFieldException
  • SecurityException

除此之外,ReflectException属于unchecked 异常,语法上不需要显式进行捕获,但是也需要根据实际情况,斟酌是否进行显式捕获该异常。

使用示例

创建实例

1
String string = Reflect.on(String.class).create("Hello World").get();

访问属性(public,protected,package,private均可)

1
char pathSeparatorChar = Reflect.on(File.class).create("/sdcard/droidyue.com").field("pathSeparatorChar").get();

修改属性(final属性也可以修改)

1
String setValue = Reflect.on(File.class).create("/sdcard/drodiyue.com").set("path", "fakepath").get("path");

调用方法(public,protected,package,private均可)

1
2
3
4
ArrayList arrayList = new ArrayList();
arrayList.add("Hello");
arrayList.add("World");
int value = Reflect.on(arrayList).call("hugeCapacity", 12).get();

实现原理

Reflect实际是对原生java reflect进行封装,屏蔽了无关细节。

以fields方法为例,其内部实现可以看出是调用了java原生提供的反射相关的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Map<String, Reflect> fields() {
    Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
    Class<?> type = type();

    do {
        for (Field field : type.getDeclaredFields()) {
            if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
                String name = field.getName();

            if (!result.containsKey(name))
              result.put(name, field(name));
            }
        }

        type = type.getSuperclass();
    } while (type != null);

    return result;
}

库地址

以上就是这些,希望jOOR可以对大家的开发日常有所帮助。

]]>
0
<![CDATA[在 Unity3D 的 Mono 虚拟机中嵌入 Lua 的一个方案]]> http://www.udpwork.com/item/16056.html http://www.udpwork.com/item/16056.html#reviews Sun, 08 Jan 2017 22:39:38 +0800 云风 http://www.udpwork.com/item/16056.html 很多使用 Unity3D 开发的项目,都不太喜欢 C# 这门开发语言,对于游戏开发很多人还是更喜欢 Lua 一些。而 Lua 作为一门嵌入式语言,嵌入别的宿主中正是它说擅长的事。这些年,我见过许多人都做过 U3D 的 Lua 嵌入方案。比如我公司的阿楠同学用纯 C# 实现了一个 Lua 5.2 (用于在 U3D web 控件中嵌入 Lua 语言的 UniLua );还有 ulua slua wlua plua xlua ... 数不胜数。我猜测,a-z 这 26 个字母早就用完了。

上面提到的项目的作者不少是我很熟悉的朋友,我们公司现在的 U3D 游戏也由同事自己实现了一套差不多的东西。所以我曾了解过这些方案。但我一直觉得这些方案要么做的过于繁琐,要么有些细节上不太完备,总是手痒想按自己的想法搞搞看。

Mono 和 C 通讯使用 P/Invoke ,用起来不算麻烦,但是要小心暗地里做的 Marshal 的代价,特别是对象传递时装箱拆箱的成本。Lua 和 C 通讯有一套完善的 C API ,但完全正确使用并不容易。核心难点是 Mono 和 Lua 各有一套自己的异常机制,让它们协调工作必须很小心的封装两个语言的边界,不要让异常漏出去。我在 2015 年写过一篇 Blog 做过相关讨论

我认为简单且完备的 Mono / Lua 交互方案是这样的:

当一边要和另一边通讯时,这和 C/S 结构的相互通讯并没有本质区别,都是发送一串数据到对方虚拟机。这种抽象方式要比 Mono 和 C 交互用的 P/Invoke 或是 Lua 的一堆 C API 要简洁的多。通常说来,一切的跨虚拟机通讯,都仅可以看成是一次异地函数调用。只要约定发送的数据串的第一项是一个函数,而后续内容是调用的参数即可。

所以 Mono 和 Lua 的交互方案就简化成了,如何从一边发送一串数据,这串数据中可以包含两边都认可的基本数据类型,如数字、字符串、布尔量,也可以包含某个虚拟机中的对象。我们并不需要真的把本地的一个对象的数据内容全部序列化成串发送给对端,而只需要给将发出的本地对象附上一个数字 id ,对端记录下 id ,等后面真的需要操作这个远程对象时,再将 id 发送回去即可。

要调用的函数本身也是一个本地对象。对于 Lua ,函数本来就是 first class 的,而 Mono 这边则可以统一给一个 Delegate 来做此媒介。

以 Mono 调用 Lua 为例,我们用事先获取到的 Lua 函数对象 id ,加上调用参数,将这一系列数据组织在一个不需要特别做 Marshal 的 struct 中,把这个 struct 通过 P/Invoke 传给 C 层;然后 C 函数调用一个写好的 Lua 函数把 struct 的内容置入 Lua VM 。然后在 Lua VM 中,用事先定义好的流程去处理它,通常的处理方式就是将第一个函数对象压栈,用后面的数据做参数调用它。最后,取得函数调用的返回值,再将返回值编码成 Mono 可操作的 struct 返回。

之所以是通过一个 struct 转换,而不是像很多别的封装方案那样把 lua 的 C API 导成 C# 的 API 直接操作 Lua 虚拟机。是因为从设计层面看,我们需要提高这个模块的内聚性,让和 Lua 交互层和 Mono 有最少的接口(减少耦合)。另,Lua 的 API 原本是供 C 使用的,对于异常处理有一套独特的规则;而掺入 Mono 这个东西后,我们又需要异常不外溢。把 struct 压入 Lua 虚拟机的过程可以用唯一一个 lua 函数做到,更方便限制住任何可能产生的异常。

Lua 调用 Mono 会稍微麻烦一点,需要定义一个 Delegate ,然后再把需要调用的 C# 函数/类等都按此 Delegate 做一些封装。好在 C# 有完善的反射机制来做这件事,若想提高效率的话,还可以有别的优化手段,比如为需要导出的类做代码生成。因为嵌入 Lua 的目的是将多变的业务放到更灵活的 Lua 语言中去编写,而 C# 这边的代码相对固定,在项目中后期基本不会有太多变化,这些优化手段都是值得在项目前期进行的。

注:这里从 Mono 返回字符串部分要小心处理。因为 Mono 向外传递字符串有额外的开销,最好能做到不传字符串时,可以没有这个开销。

这个周末,我花花整整一天的时间来实现上面的想法。代码放在了 github 上。它可以在 mono 上编译运行,暂时没有文档,但是整个结构很简单,使用范例在 test.cs 里也基本展示出来了。

这里花去不少篇幅完成的工作是两个不同虚拟机间的对象相互引用。之前在xlua 的项目 issue 中做了一些讨论

一个虚拟机的对象,如果传递到另一边,需要在本地做一个强引用,防止被 gc 掉。当对方不再使用这个对象后,可以解除这个强引用。对于远程对象,在本地都是记录一个 id 。Lua 和 C# 都有发现一个对象不再使用的能力,Lua 利用的是弱表,C# 有 Weak Reference 。以 Lua 为例,我们将远程对象放在弱表中,以 id 去索引;同时再把远程对象的 id 都收集在一个集合里。只需要定期检查 id 集合中有哪些 id 于弱表中查询不到了,它们就是不再使用的远程对象。

固然,还可以用__gc方法在远程代理对象被回收时获知信息,但我并不推荐这种做法。加上__gc方法会为 gc 流程增加许多不必要的负担,而且这些方法的调用时机很难主动掌控,最终你还是只会在__gc方法中登记一下 id ,和上面提到的主动比对弱表的方案并没有获得任何好处。

真正难处理的地方在于两个虚拟机间对象的循环引用。

假设 mono 中有一个对象 A 被传递到 Lua ,Lua 中为之生成了代理 A' ;Lua 中有另一个对象 B 传递给 Mono ,Mono 为之生成了代理对象 B' 。

如果 mono 中 A 引用了 B' ,同时 Lua 中 B 引用了 A' ,则造成了循环引用。由于 Lua 中的 A' 不回收的话,Mono 不能回收 A ;同理 Mono 中的 B' 不回收的话, Lua 中也会一直持有 B 的强引用。所以 A B 两个对象即使没有任何别的地方使用它们了,也无法被回收掉。

回收这类循环引用的对象也并非没有办法。如果虚拟机具备一种能力,可以获知一个对象是否只被特定东西(在这里指外部虚拟机)引用住,那么就可以很简单的解决这个问题。

当 Mono / Lua 发现,某些对象仅存在外部引用,那么就将这些对象设置成一个特殊状态(可以是引用次数加一,也可以是放在一个特殊集合中);一旦某个对象被设置了两次特殊状态(双方都不再引用),就可以真的清除它们。

我对 C# 不太熟悉,不知道如何做到这点;但 Lua 做这件事情非常容易。

一种方法是,自己遍历虚拟机,但不遍历导出对象的集合,所有没有遍历到的,但存在于这个集合中的对象,就是仅有外部引用的。遍历虚拟机对 Lua 来说不是难事,我在两个过去的项目中分别用LuaC各实现过一遍。

还有一种取巧的方法需要利用 Lua 的ephemeron table。当我们需要检测一个对象是否只有外部引用时,可以先把它从引用表里移除,移到一个 ephemeron table 中。这个 table 的结构是 obj : { obj } 这个样子。对于 { obj } 这个 value 可以加上__gc方法。如果 obj 没有额外的引用,那么__gc会被调用。我们可以把 obj 移到另一个叫做坟场的 table 中复活。这样 obj 就没有真的被清理掉了。

不过采用这个方法时,要特别留意 weak table (ephemeron table) 在工作时,会让暂时移除的 obj 处于一种中间状态,即不在 weak table 中,__gc也还没有被调用,也就是没来得及移到坟场。

仅使用 Lua 这种检测能力,就足以消除循环引用。当我们找到只有外部引用的对象,就可以认为在当次 gc 循环结束后,这批对象没有内部引用了,它们只有外部引用,且相互间可能有联系(即前面说的, A B 间有循环引用)。

这批对象暂时不能从 Lua 中删除,因为 C# 一侧可能还持有它们的引用,日后会访问它们。但 Lua 中目前已经没有引用了,可以把这些对象的删除请求发送给 Mono 。Mono 收到后,可以解除这批对象的外部引用(解开循环引用),等待 GC 工作;如果其中有对象真的被回收,再通知 Lua 真的删除掉。如果 C# 还在继续引用,则通知 Lua 把对象全部从坟场取回。

方案细节在前面给出的 issue 中已经讨论的足够多了,这里不再展开。

我们真的需要这么细致的管理双向引用么?

在我们自己的项目中,并没有做这些复杂处理。这是因为,一旦在 C# 中加入 Lua ,就暗示着把业务逻辑搬到了 Lua 中写。在 Mono 和 Lua 两边都存在业务逻辑且交叉引用的情况本身就是很不合理的。更多的情况是,Mono 负责和引擎底层沟通,所有的引擎对象都是由 Lua 通过中间城命令 C# 去创建的;当 Lua 层不再使用这些对象后,再通知删除。C# 本身并没有业务层去引用这些对象。Lua 和 C# 应该是应该上下层清晰的关系,而不应该是混杂在一起的并列关系。

所以我推荐的做法是,只有 Lua 可以长期持有 Mono 中的 C# 对象,而 Mono 中只可以短期持有 Lua 层的对象(不超过游戏中的一帧)。这样,Lua 就有权利主动清理那些自己并不持有的本地对象而不需要通知 Mono 了,这种单边关系便不会产生循环引用。

Mono 中唯一可能长期持有的 Lua 对象唯有一些重要的回调函数,比如在每个游戏逻辑帧内都去调用一次 Lua 里定义好的 update 函数。而这种 Lua 函数对象,只需要让 Lua 自己长期保有引用(比如放在全局表里)就可以了。

即使真的想做出一套完备的 Mono 和 Lua 间的对象双向引用关系,我也推荐用最简单的方案,基础方案中不去考虑循环引用的问题。而可以单独写一个模块来解开潜在的循环引用,这个模块性能不是主要考虑问题,在合适的时候(比如 loading 场景时)启动检查即可。

最后简单说说我周末实现的这套 sharplua 。它提供了在 Mono 中创建出一个 Lua 虚拟机,并可以从 C# 调用 Lua 函数,获取返回值的能力。同时,Lua 代码中也可以调用由 C# 注入的 C# 函数。

SharpLua 类即对应一个 lua 5.3 虚拟机,需要传入第一个 lua 文件名启动它。这个 lua 文件中必须 require "sharplua" 这个模块,辅助完成初始化工作。sharplua 这个 lua 模块中有部分是用来管理 mono 和 lua 间数据交换的内部函数,供底层工作时使用;还有一些提供给 lua 业务层使用的 api ,方便回调 C# 函数。

C# 这边只有三个 API 用来和 Lua 通讯。

可以通过 SharpLua.GetFunction 从 Lua 虚拟机的全局表中获得一个以字符串命名的全局函数。这是一切逻辑的起点。之所以不提供更多的获取 Lua 内部数据的 C# API 是因为,其他的需求都可以通过你自己写一个 Lua 全局函数来完成,C# 只需要调用它就可以了。

SharpLua.CallFunction 可以用来调用一个 Lua 函数,携带任意参数,可获得任意返回值。为了实现简单,这里限制了一次函数调用最多传 255 个参数,返回值不能超过 256 个。

注意,返回值也可以是一个 Lua 函数对象。所以你可以写一个 Lua 全局函数来返回 Lua 虚拟机中的其它函数。而参数则可以是任意对象,除了数字、字符串等这些 Mono 和 Lua 都有的基本类型外,还可以传入之前的获取的 Lua 对象以及 C# 的任意 Class 对象。这里约定了一种指定的 Delegate ,一旦把它传个 Lua ,Lua 可以通过 sharplua.call 来回调它,从而可以做到 Lua 向 C# 通讯。具体用法可以参考 test.cs ,虽然这里是手写了一个 Delegate 供 Lua 调用,但是你可以继续完善它,比如使用 C# 的反射能力去间接调用任何你想调用的 C# 函数,也可以为 C# 类做一些代码生成工作,生成函数以这个 Delegate 的形式注入 Lua 。

最后一个 API 是 SharpLua.CollectGarbage 。它会从 Lua 虚拟机中收集那些曾经传给 Lua 的 C# 对象中,哪些 Lua 已经不再使用,好让 Mono 这边可以解除引用让 Mono 的 GC 可以正确工作以回收掉它们。

SharpLua 它整个实现简单易读,对外接口也很少。稍加封装,就可以嵌入 Unity3D 中使用。如果有同学有兴趣继续完善,欢迎提 PR 。

有几点是可以继续做的。

  1. C# 的字符串最好能 marshal 成 Unicode ,然后在 Lua 里转换成 utf8 ;还有相关的反向处理。

  2. 在 marshal 字符串的时候,如果发现是短字符串,可以在 mono 和 lua 间同步一张不太大的字符串表,只在第一次传递的时候对 string 做 marshal ,之后相同的字符串都查表传 id ,减轻 string 传递的负担。

]]>
很多使用 Unity3D 开发的项目,都不太喜欢 C# 这门开发语言,对于游戏开发很多人还是更喜欢 Lua 一些。而 Lua 作为一门嵌入式语言,嵌入别的宿主中正是它说擅长的事。这些年,我见过许多人都做过 U3D 的 Lua 嵌入方案。比如我公司的阿楠同学用纯 C# 实现了一个 Lua 5.2 (用于在 U3D web 控件中嵌入 Lua 语言的 UniLua );还有 ulua slua wlua plua xlua ... 数不胜数。我猜测,a-z 这 26 个字母早就用完了。

上面提到的项目的作者不少是我很熟悉的朋友,我们公司现在的 U3D 游戏也由同事自己实现了一套差不多的东西。所以我曾了解过这些方案。但我一直觉得这些方案要么做的过于繁琐,要么有些细节上不太完备,总是手痒想按自己的想法搞搞看。

Mono 和 C 通讯使用 P/Invoke ,用起来不算麻烦,但是要小心暗地里做的 Marshal 的代价,特别是对象传递时装箱拆箱的成本。Lua 和 C 通讯有一套完善的 C API ,但完全正确使用并不容易。核心难点是 Mono 和 Lua 各有一套自己的异常机制,让它们协调工作必须很小心的封装两个语言的边界,不要让异常漏出去。我在 2015 年写过一篇 Blog 做过相关讨论

我认为简单且完备的 Mono / Lua 交互方案是这样的:

当一边要和另一边通讯时,这和 C/S 结构的相互通讯并没有本质区别,都是发送一串数据到对方虚拟机。这种抽象方式要比 Mono 和 C 交互用的 P/Invoke 或是 Lua 的一堆 C API 要简洁的多。通常说来,一切的跨虚拟机通讯,都仅可以看成是一次异地函数调用。只要约定发送的数据串的第一项是一个函数,而后续内容是调用的参数即可。

所以 Mono 和 Lua 的交互方案就简化成了,如何从一边发送一串数据,这串数据中可以包含两边都认可的基本数据类型,如数字、字符串、布尔量,也可以包含某个虚拟机中的对象。我们并不需要真的把本地的一个对象的数据内容全部序列化成串发送给对端,而只需要给将发出的本地对象附上一个数字 id ,对端记录下 id ,等后面真的需要操作这个远程对象时,再将 id 发送回去即可。

要调用的函数本身也是一个本地对象。对于 Lua ,函数本来就是 first class 的,而 Mono 这边则可以统一给一个 Delegate 来做此媒介。

以 Mono 调用 Lua 为例,我们用事先获取到的 Lua 函数对象 id ,加上调用参数,将这一系列数据组织在一个不需要特别做 Marshal 的 struct 中,把这个 struct 通过 P/Invoke 传给 C 层;然后 C 函数调用一个写好的 Lua 函数把 struct 的内容置入 Lua VM 。然后在 Lua VM 中,用事先定义好的流程去处理它,通常的处理方式就是将第一个函数对象压栈,用后面的数据做参数调用它。最后,取得函数调用的返回值,再将返回值编码成 Mono 可操作的 struct 返回。

之所以是通过一个 struct 转换,而不是像很多别的封装方案那样把 lua 的 C API 导成 C# 的 API 直接操作 Lua 虚拟机。是因为从设计层面看,我们需要提高这个模块的内聚性,让和 Lua 交互层和 Mono 有最少的接口(减少耦合)。另,Lua 的 API 原本是供 C 使用的,对于异常处理有一套独特的规则;而掺入 Mono 这个东西后,我们又需要异常不外溢。把 struct 压入 Lua 虚拟机的过程可以用唯一一个 lua 函数做到,更方便限制住任何可能产生的异常。

Lua 调用 Mono 会稍微麻烦一点,需要定义一个 Delegate ,然后再把需要调用的 C# 函数/类等都按此 Delegate 做一些封装。好在 C# 有完善的反射机制来做这件事,若想提高效率的话,还可以有别的优化手段,比如为需要导出的类做代码生成。因为嵌入 Lua 的目的是将多变的业务放到更灵活的 Lua 语言中去编写,而 C# 这边的代码相对固定,在项目中后期基本不会有太多变化,这些优化手段都是值得在项目前期进行的。

注:这里从 Mono 返回字符串部分要小心处理。因为 Mono 向外传递字符串有额外的开销,最好能做到不传字符串时,可以没有这个开销。

这个周末,我花花整整一天的时间来实现上面的想法。代码放在了 github 上。它可以在 mono 上编译运行,暂时没有文档,但是整个结构很简单,使用范例在 test.cs 里也基本展示出来了。

这里花去不少篇幅完成的工作是两个不同虚拟机间的对象相互引用。之前在xlua 的项目 issue 中做了一些讨论

一个虚拟机的对象,如果传递到另一边,需要在本地做一个强引用,防止被 gc 掉。当对方不再使用这个对象后,可以解除这个强引用。对于远程对象,在本地都是记录一个 id 。Lua 和 C# 都有发现一个对象不再使用的能力,Lua 利用的是弱表,C# 有 Weak Reference 。以 Lua 为例,我们将远程对象放在弱表中,以 id 去索引;同时再把远程对象的 id 都收集在一个集合里。只需要定期检查 id 集合中有哪些 id 于弱表中查询不到了,它们就是不再使用的远程对象。

固然,还可以用__gc方法在远程代理对象被回收时获知信息,但我并不推荐这种做法。加上__gc方法会为 gc 流程增加许多不必要的负担,而且这些方法的调用时机很难主动掌控,最终你还是只会在__gc方法中登记一下 id ,和上面提到的主动比对弱表的方案并没有获得任何好处。

真正难处理的地方在于两个虚拟机间对象的循环引用。

假设 mono 中有一个对象 A 被传递到 Lua ,Lua 中为之生成了代理 A' ;Lua 中有另一个对象 B 传递给 Mono ,Mono 为之生成了代理对象 B' 。

如果 mono 中 A 引用了 B' ,同时 Lua 中 B 引用了 A' ,则造成了循环引用。由于 Lua 中的 A' 不回收的话,Mono 不能回收 A ;同理 Mono 中的 B' 不回收的话, Lua 中也会一直持有 B 的强引用。所以 A B 两个对象即使没有任何别的地方使用它们了,也无法被回收掉。

回收这类循环引用的对象也并非没有办法。如果虚拟机具备一种能力,可以获知一个对象是否只被特定东西(在这里指外部虚拟机)引用住,那么就可以很简单的解决这个问题。

当 Mono / Lua 发现,某些对象仅存在外部引用,那么就将这些对象设置成一个特殊状态(可以是引用次数加一,也可以是放在一个特殊集合中);一旦某个对象被设置了两次特殊状态(双方都不再引用),就可以真的清除它们。

我对 C# 不太熟悉,不知道如何做到这点;但 Lua 做这件事情非常容易。

一种方法是,自己遍历虚拟机,但不遍历导出对象的集合,所有没有遍历到的,但存在于这个集合中的对象,就是仅有外部引用的。遍历虚拟机对 Lua 来说不是难事,我在两个过去的项目中分别用LuaC各实现过一遍。

还有一种取巧的方法需要利用 Lua 的ephemeron table。当我们需要检测一个对象是否只有外部引用时,可以先把它从引用表里移除,移到一个 ephemeron table 中。这个 table 的结构是 obj : { obj } 这个样子。对于 { obj } 这个 value 可以加上__gc方法。如果 obj 没有额外的引用,那么__gc会被调用。我们可以把 obj 移到另一个叫做坟场的 table 中复活。这样 obj 就没有真的被清理掉了。

不过采用这个方法时,要特别留意 weak table (ephemeron table) 在工作时,会让暂时移除的 obj 处于一种中间状态,即不在 weak table 中,__gc也还没有被调用,也就是没来得及移到坟场。

仅使用 Lua 这种检测能力,就足以消除循环引用。当我们找到只有外部引用的对象,就可以认为在当次 gc 循环结束后,这批对象没有内部引用了,它们只有外部引用,且相互间可能有联系(即前面说的, A B 间有循环引用)。

这批对象暂时不能从 Lua 中删除,因为 C# 一侧可能还持有它们的引用,日后会访问它们。但 Lua 中目前已经没有引用了,可以把这些对象的删除请求发送给 Mono 。Mono 收到后,可以解除这批对象的外部引用(解开循环引用),等待 GC 工作;如果其中有对象真的被回收,再通知 Lua 真的删除掉。如果 C# 还在继续引用,则通知 Lua 把对象全部从坟场取回。

方案细节在前面给出的 issue 中已经讨论的足够多了,这里不再展开。

我们真的需要这么细致的管理双向引用么?

在我们自己的项目中,并没有做这些复杂处理。这是因为,一旦在 C# 中加入 Lua ,就暗示着把业务逻辑搬到了 Lua 中写。在 Mono 和 Lua 两边都存在业务逻辑且交叉引用的情况本身就是很不合理的。更多的情况是,Mono 负责和引擎底层沟通,所有的引擎对象都是由 Lua 通过中间城命令 C# 去创建的;当 Lua 层不再使用这些对象后,再通知删除。C# 本身并没有业务层去引用这些对象。Lua 和 C# 应该是应该上下层清晰的关系,而不应该是混杂在一起的并列关系。

所以我推荐的做法是,只有 Lua 可以长期持有 Mono 中的 C# 对象,而 Mono 中只可以短期持有 Lua 层的对象(不超过游戏中的一帧)。这样,Lua 就有权利主动清理那些自己并不持有的本地对象而不需要通知 Mono 了,这种单边关系便不会产生循环引用。

Mono 中唯一可能长期持有的 Lua 对象唯有一些重要的回调函数,比如在每个游戏逻辑帧内都去调用一次 Lua 里定义好的 update 函数。而这种 Lua 函数对象,只需要让 Lua 自己长期保有引用(比如放在全局表里)就可以了。

即使真的想做出一套完备的 Mono 和 Lua 间的对象双向引用关系,我也推荐用最简单的方案,基础方案中不去考虑循环引用的问题。而可以单独写一个模块来解开潜在的循环引用,这个模块性能不是主要考虑问题,在合适的时候(比如 loading 场景时)启动检查即可。

最后简单说说我周末实现的这套 sharplua 。它提供了在 Mono 中创建出一个 Lua 虚拟机,并可以从 C# 调用 Lua 函数,获取返回值的能力。同时,Lua 代码中也可以调用由 C# 注入的 C# 函数。

SharpLua 类即对应一个 lua 5.3 虚拟机,需要传入第一个 lua 文件名启动它。这个 lua 文件中必须 require "sharplua" 这个模块,辅助完成初始化工作。sharplua 这个 lua 模块中有部分是用来管理 mono 和 lua 间数据交换的内部函数,供底层工作时使用;还有一些提供给 lua 业务层使用的 api ,方便回调 C# 函数。

C# 这边只有三个 API 用来和 Lua 通讯。

可以通过 SharpLua.GetFunction 从 Lua 虚拟机的全局表中获得一个以字符串命名的全局函数。这是一切逻辑的起点。之所以不提供更多的获取 Lua 内部数据的 C# API 是因为,其他的需求都可以通过你自己写一个 Lua 全局函数来完成,C# 只需要调用它就可以了。

SharpLua.CallFunction 可以用来调用一个 Lua 函数,携带任意参数,可获得任意返回值。为了实现简单,这里限制了一次函数调用最多传 255 个参数,返回值不能超过 256 个。

注意,返回值也可以是一个 Lua 函数对象。所以你可以写一个 Lua 全局函数来返回 Lua 虚拟机中的其它函数。而参数则可以是任意对象,除了数字、字符串等这些 Mono 和 Lua 都有的基本类型外,还可以传入之前的获取的 Lua 对象以及 C# 的任意 Class 对象。这里约定了一种指定的 Delegate ,一旦把它传个 Lua ,Lua 可以通过 sharplua.call 来回调它,从而可以做到 Lua 向 C# 通讯。具体用法可以参考 test.cs ,虽然这里是手写了一个 Delegate 供 Lua 调用,但是你可以继续完善它,比如使用 C# 的反射能力去间接调用任何你想调用的 C# 函数,也可以为 C# 类做一些代码生成工作,生成函数以这个 Delegate 的形式注入 Lua 。

最后一个 API 是 SharpLua.CollectGarbage 。它会从 Lua 虚拟机中收集那些曾经传给 Lua 的 C# 对象中,哪些 Lua 已经不再使用,好让 Mono 这边可以解除引用让 Mono 的 GC 可以正确工作以回收掉它们。

SharpLua 它整个实现简单易读,对外接口也很少。稍加封装,就可以嵌入 Unity3D 中使用。如果有同学有兴趣继续完善,欢迎提 PR 。

有几点是可以继续做的。

  1. C# 的字符串最好能 marshal 成 Unicode ,然后在 Lua 里转换成 utf8 ;还有相关的反向处理。

  2. 在 marshal 字符串的时候,如果发现是短字符串,可以在 mono 和 lua 间同步一张不太大的字符串表,只在第一次传递的时候对 string 做 marshal ,之后相同的字符串都查表传 id ,减轻 string 传递的负担。

]]>
0
<![CDATA[折腾 Linux 4.9 BBR 拥堵控制算法]]> http://www.udpwork.com/item/16054.html http://www.udpwork.com/item/16054.html#reviews Sun, 08 Jan 2017 08:00:00 +0800 Dozer Zone http://www.udpwork.com/item/16054.html 新年折腾 BBR

人是一种很奇怪的东西,以前家里科学上网速度很慢的时候,只要能打开就很满意了。

现在换了联通,Google 明明已经秒开了,却又开始不满足现状了。

恰巧最近看到了各种 KCP, BBR 技术的介绍,就想给自己的梯子折腾折腾了。

 

KCP 介绍

KCP 是开源社区的一个新轮子,看 commit 记录是从 2014 年开始开发的。算是一个很新的东西了。

KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。

项目地址:https://github.com/skywind3000/kcp

更多的内容就不详细介绍了,项目主页写得非常详细了。

 

该项目只是提供了 KCP 的核心算法,而没有对应的应用。如果要用起来的话,可以利用如下两个项目:

 

kcptun: A Simple UDP Tunnel Based On KCP

项目地址:https://github.com/xtaci/kcptun

和锐速之类的软件一样,可以在底层使用 SS,然后再用 KCP 通道加速。

 

v2ray: 自称是 SS 的替代者

项目地址:https://www.v2ray.com/

内置 KCP 协议,需要自己搭建,不依赖额外的组建。

 

KCP 相关的东西就不多介绍了,上面提到的各种产品都体验过。东西都很新,所以其实都还不成熟。

而且最大的问题是,没有好用的客户端,从终端启动也是挺麻烦的。

而且实测也并没有比 SS 快多少,所以就放弃了。

 

Linux 4.9 BBR 拥堵算法介绍

为什么它能为你的 SS 提速?因为之前的 TCP 拥堵算法都是基于丢包的,丢包多了它就认为带宽不够了。

而我们伟大的长城正好利用了这个特性,最终达到了慢却又不是完全不可用的状态。

知乎上有人解答的非常好,一下子就看懂了。

传送门:https://www.zhihu.com/question/53559433

 

第一次看到 Linux 4.9 我就吓尿了,我之前的 VPS 才 2.6 啊…

之前一直在用 bandwagonhost 高性价比机器,每年不到 100,机器速度也非常不多。唯一的缺点就是它是基于 OpenVZ 架构的。

意味着不能自己更换 Linux 内核。也就是说,继续用 bandwagonhost 的话就无法启用 Linux 4.9 了。

 

另外,同样很新为什么我更愿意用这个?因为它只是修改了服务器的内核,相关的工具包括所有的客户端都不需要做任何修改。因此部署起来方便,以后不用了也方便。

 

寻找好机器

既然 bandwagonhost 无法支持,那么第一步就是要找一台好机器了。网上搜寻一番后,发现 vultr 还不错。

  • 每月 $5,可按小时收费
  • 支持 API 调用,可以在不需要的时候关闭机器节约成本
  • 全球各地都有机房,试了一下日本机房速度还不错
  • KVM 架构,可以自己换内核

利用此链接注册,首次充值可多得 $20。

 

升级内核,启用 BBR

简单介绍一下 Ubuntu x64 下操作方式:

    # 下载并安装内核
    cd /tmp/
    http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-headers-4.9.0-040900_4.9.0-040900.201612111631_all.deb
    http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-headers-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb
    http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-image-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb
    sudo dpkg -i *.deb

    # 重启
    reboot

    # 查看是否安装成功
    uname -r

    # 启用 BBR 拥堵控制算法
    echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
    echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
    sysctl -p

    # 查看是否启用成功,如果出现 BBR 就代表成功
    sysctl net.ipv4.tcp_available_congestion_control

整个过程非常简单,其他系统可参考这篇文章:传送门

]]>
新年折腾 BBR

人是一种很奇怪的东西,以前家里科学上网速度很慢的时候,只要能打开就很满意了。

现在换了联通,Google 明明已经秒开了,却又开始不满足现状了。

恰巧最近看到了各种 KCP, BBR 技术的介绍,就想给自己的梯子折腾折腾了。

 

KCP 介绍

KCP 是开源社区的一个新轮子,看 commit 记录是从 2014 年开始开发的。算是一个很新的东西了。

KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。

项目地址:https://github.com/skywind3000/kcp

更多的内容就不详细介绍了,项目主页写得非常详细了。

 

该项目只是提供了 KCP 的核心算法,而没有对应的应用。如果要用起来的话,可以利用如下两个项目:

 

kcptun: A Simple UDP Tunnel Based On KCP

项目地址:https://github.com/xtaci/kcptun

和锐速之类的软件一样,可以在底层使用 SS,然后再用 KCP 通道加速。

 

v2ray: 自称是 SS 的替代者

项目地址:https://www.v2ray.com/

内置 KCP 协议,需要自己搭建,不依赖额外的组建。

 

KCP 相关的东西就不多介绍了,上面提到的各种产品都体验过。东西都很新,所以其实都还不成熟。

而且最大的问题是,没有好用的客户端,从终端启动也是挺麻烦的。

而且实测也并没有比 SS 快多少,所以就放弃了。

 

Linux 4.9 BBR 拥堵算法介绍

为什么它能为你的 SS 提速?因为之前的 TCP 拥堵算法都是基于丢包的,丢包多了它就认为带宽不够了。

而我们伟大的长城正好利用了这个特性,最终达到了慢却又不是完全不可用的状态。

知乎上有人解答的非常好,一下子就看懂了。

传送门:https://www.zhihu.com/question/53559433

 

第一次看到 Linux 4.9 我就吓尿了,我之前的 VPS 才 2.6 啊…

之前一直在用 bandwagonhost 高性价比机器,每年不到 100,机器速度也非常不多。唯一的缺点就是它是基于 OpenVZ 架构的。

意味着不能自己更换 Linux 内核。也就是说,继续用 bandwagonhost 的话就无法启用 Linux 4.9 了。

 

另外,同样很新为什么我更愿意用这个?因为它只是修改了服务器的内核,相关的工具包括所有的客户端都不需要做任何修改。因此部署起来方便,以后不用了也方便。

 

寻找好机器

既然 bandwagonhost 无法支持,那么第一步就是要找一台好机器了。网上搜寻一番后,发现 vultr 还不错。

  • 每月 $5,可按小时收费
  • 支持 API 调用,可以在不需要的时候关闭机器节约成本
  • 全球各地都有机房,试了一下日本机房速度还不错
  • KVM 架构,可以自己换内核

利用此链接注册,首次充值可多得 $20。

 

升级内核,启用 BBR

简单介绍一下 Ubuntu x64 下操作方式:

    # 下载并安装内核
    cd /tmp/
    http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-headers-4.9.0-040900_4.9.0-040900.201612111631_all.deb
    http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-headers-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb
    http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-image-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb
    sudo dpkg -i *.deb

    # 重启
    reboot

    # 查看是否安装成功
    uname -r

    # 启用 BBR 拥堵控制算法
    echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
    echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
    sysctl -p

    # 查看是否启用成功,如果出现 BBR 就代表成功
    sysctl net.ipv4.tcp_available_congestion_control

整个过程非常简单,其他系统可参考这篇文章:传送门

]]>
0
<![CDATA[ReactJS项目中基于webpack实现页面插件]]> http://www.udpwork.com/item/16055.html http://www.udpwork.com/item/16055.html#reviews Sun, 08 Jan 2017 00:00:00 +0800 Kevin Lynx http://www.udpwork.com/item/16055.html 整个Web页面是基于ReactJS的,js打包用的webpack,现在想在Web页面端实现一种插件机制,可以动态载入第三方写的js插件。这个插件有一个约定的入口,插件被载入后调用该入口函数,插件内部实现渲染逻辑。插件的实现也使用了ReactJS,当然理论上也可以不使用。预期的交互关系是这样的:

1
2
3
4
5
6
7
8
9
10
// 主页面
load('/plugin/my-plugin.js', function (plugin) {
    plugin.init($('#plugin-main'), args)
})

// 基于ReactJS的插件
function init($elem, args) {
    ReactDOM.render((<Index />), $elem)
}
export {init}

在主页面上支持这种插件机制,有点类似一个应用市场,主页面作为应用平台,插件就是应用,用户可以在主页面上选用各种插件。

问题

目前主页面里ReactJS被webpack打包进了bundle.js,如果插件也把ReactjS打包进去,最终在载入插件后,浏览器环境中就会初始化两次ReactJS。而ReactJS是不能被初始化多次的 。此外,为了插件编写方便,我把一些可重用的组件打包成一个单独的库,让主页面和插件都去依赖。这个库自然也不能把ReactJS打包进来。何况还有很多三方库,例如underscore、ReactDOM最好也能避免重复打包,从而可以去除重复的内容。所以,这里就涉及到如何在webpack中拆分这些库。

需要解决的问题:

  • 拆分三方库,避免打包进bundle.js
  • 动态载入js文件,且能拿到其module,或者至少能知道js什么时候被载入,才能调用其入口函数

关于第二个问题,我选用了RequireJS,但其实它不是用于我这种场景的,不过我不想自己写一个js载入器。用RequireJS在我这种场景下会带来一些问题:webpack在打包js文件时会检查是否有AMD模块加载器,如果有则会把打包的代码作为AMD模块来加载。对于三方库的依赖就需要做一些适配。

实现

开始做这件事时我是不熟悉RequireJS/AMD的,导致踩了不少坑。过程不表,这里就记录一些关键步骤。

公共组件库及插件是必须要打包为library的,否则没有导出符号:

1
2
3
4
5
6
7
// webpack.config.js
config.output = {
  filename: 'drogo_components.js',
  path: path.join(__dirname, 'dist'),
  libraryTarget: 'umd',
  library: 'drogo_components'
};

此外,为了不打包三方库进bundle.js,需要设置:

1
2
3
4
5
// webpack.config.js
config.externals = {
  'react': 'React',
  'underscore': '_',
};

externals中key为代码中require或import xxx from 'xxx'中的名字,value为输出代码中的名字。以上设置后,webpack打包出来的代码类似于:

1
2
3
4
5
6
(function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory(require("React"), require("_"));
    else if(typeof define === 'function' && define.amd)
        define(["React", "_"], factory);
...

了解了RequireJS后就能看懂上面的代码,意思是定义我这里说的插件或公共库为一个模块,其依赖React及_模块。

插件及公共库如何编写?

1
2
3
4
5
6
7
8
9
10
11
// 入口main.js中
import React from 'react'
import ReactDOM from 'react-dom'
import Test from './components/test'
import Index from './components/index'

function init($elem, data) {
    ReactDOM.render((<Index biz={data.biz} />), $elem)
}

export {Index, Test, init}

入口js中export的内容就会成为这个library被require载入后能拿到的符号。这个库在webpack中引用时同理。注意需要设置库的入口文件:

1
2
// package.json
  "main": "static/js/main.bundle.js",

对于本地库,可以通过以下方法在本地使用:

1
2
3
4
// 打包本地库,生成库.tgz文件
npm pack
// 切换到要使用该库的工程下安装
npm install ../xxx/xxx.tgz

package.json中也不需要依赖该文件,如果不自己install,也是可以在package.json中依赖的,类似:

1
"xxxx": "file:../xxx/xxx.tgz"

经过以上步骤后,在主页面中载入插件打包的bundle.js时,会得到错误,说找不到React模块。我这里并没有完全改造为RequireJS的模块,所以我在页面中是静态引入react的,即:

1
2
<script src="static/js/react-with-addons.js"></script>
<script src="static/js/react-dom.min.js"></script>

当执行插件后,RequireJS会去重新载入react.js,如果能load成功,就又会导致浏览器环境中出现两份ReactJS,解决方法是:

1
2
3
4
5
6
7
8
9
10
11
define('react', [], function() {
  return React
})

define('react-dom', [], function() {
  return ReactDOM
})

define('_', [], function () {
  return _
})

即,因为react被静态引入,就会存在全局变量window.React,所以这里是告诉RequireJS我们定义react模块就是全局变量React。此时webpack中打出的文件中require(['react'], xx时,就不会导致RequireJS再去从服务端载入react.js文件。

使用RequireJS后,要动态载入插件,代码就类似于:

1
2
3
window.require(['/api/plug/content/1'], function (m) {
  m.init($('#app-main')[0], args)
})

最后,之所以没有把页面全部改造为RequireJS,例如通过require载入主页面,主页面依赖react、公共组件库等,是因为我发现RequireJS的载入顺序与项目中使用的部分界面库有冲突,导致一些<a>的事件监听丢失(如下拉菜单不可用),根本原因还没找到。

原文地址:http://codemacro.com/2017/01/08/react-plugin/
written byKevin Lynx posted athttp://codemacro.com

]]>
整个Web页面是基于ReactJS的,js打包用的webpack,现在想在Web页面端实现一种插件机制,可以动态载入第三方写的js插件。这个插件有一个约定的入口,插件被载入后调用该入口函数,插件内部实现渲染逻辑。插件的实现也使用了ReactJS,当然理论上也可以不使用。预期的交互关系是这样的:

1
2
3
4
5
6
7
8
9
10
// 主页面
load('/plugin/my-plugin.js', function (plugin) {
    plugin.init($('#plugin-main'), args)
})

// 基于ReactJS的插件
function init($elem, args) {
    ReactDOM.render((<Index />), $elem)
}
export {init}

在主页面上支持这种插件机制,有点类似一个应用市场,主页面作为应用平台,插件就是应用,用户可以在主页面上选用各种插件。

问题

目前主页面里ReactJS被webpack打包进了bundle.js,如果插件也把ReactjS打包进去,最终在载入插件后,浏览器环境中就会初始化两次ReactJS。而ReactJS是不能被初始化多次的 。此外,为了插件编写方便,我把一些可重用的组件打包成一个单独的库,让主页面和插件都去依赖。这个库自然也不能把ReactJS打包进来。何况还有很多三方库,例如underscore、ReactDOM最好也能避免重复打包,从而可以去除重复的内容。所以,这里就涉及到如何在webpack中拆分这些库。

需要解决的问题:

  • 拆分三方库,避免打包进bundle.js
  • 动态载入js文件,且能拿到其module,或者至少能知道js什么时候被载入,才能调用其入口函数

关于第二个问题,我选用了RequireJS,但其实它不是用于我这种场景的,不过我不想自己写一个js载入器。用RequireJS在我这种场景下会带来一些问题:webpack在打包js文件时会检查是否有AMD模块加载器,如果有则会把打包的代码作为AMD模块来加载。对于三方库的依赖就需要做一些适配。

实现

开始做这件事时我是不熟悉RequireJS/AMD的,导致踩了不少坑。过程不表,这里就记录一些关键步骤。

公共组件库及插件是必须要打包为library的,否则没有导出符号:

1
2
3
4
5
6
7
// webpack.config.js
config.output = {
  filename: 'drogo_components.js',
  path: path.join(__dirname, 'dist'),
  libraryTarget: 'umd',
  library: 'drogo_components'
};

此外,为了不打包三方库进bundle.js,需要设置:

1
2
3
4
5
// webpack.config.js
config.externals = {
  'react': 'React',
  'underscore': '_',
};

externals中key为代码中require或import xxx from 'xxx'中的名字,value为输出代码中的名字。以上设置后,webpack打包出来的代码类似于:

1
2
3
4
5
6
(function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory(require("React"), require("_"));
    else if(typeof define === 'function' && define.amd)
        define(["React", "_"], factory);
...

了解了RequireJS后就能看懂上面的代码,意思是定义我这里说的插件或公共库为一个模块,其依赖React及_模块。

插件及公共库如何编写?

1
2
3
4
5
6
7
8
9
10
11
// 入口main.js中
import React from 'react'
import ReactDOM from 'react-dom'
import Test from './components/test'
import Index from './components/index'

function init($elem, data) {
    ReactDOM.render((<Index biz={data.biz} />), $elem)
}

export {Index, Test, init}

入口js中export的内容就会成为这个library被require载入后能拿到的符号。这个库在webpack中引用时同理。注意需要设置库的入口文件:

1
2
// package.json
  "main": "static/js/main.bundle.js",

对于本地库,可以通过以下方法在本地使用:

1
2
3
4
// 打包本地库,生成库.tgz文件
npm pack
// 切换到要使用该库的工程下安装
npm install ../xxx/xxx.tgz

package.json中也不需要依赖该文件,如果不自己install,也是可以在package.json中依赖的,类似:

1
"xxxx": "file:../xxx/xxx.tgz"

经过以上步骤后,在主页面中载入插件打包的bundle.js时,会得到错误,说找不到React模块。我这里并没有完全改造为RequireJS的模块,所以我在页面中是静态引入react的,即:

1
2
<script src="static/js/react-with-addons.js"></script>
<script src="static/js/react-dom.min.js"></script>

当执行插件后,RequireJS会去重新载入react.js,如果能load成功,就又会导致浏览器环境中出现两份ReactJS,解决方法是:

1
2
3
4
5
6
7
8
9
10
11
define('react', [], function() {
  return React
})

define('react-dom', [], function() {
  return ReactDOM
})

define('_', [], function () {
  return _
})

即,因为react被静态引入,就会存在全局变量window.React,所以这里是告诉RequireJS我们定义react模块就是全局变量React。此时webpack中打出的文件中require(['react'], xx时,就不会导致RequireJS再去从服务端载入react.js文件。

使用RequireJS后,要动态载入插件,代码就类似于:

1
2
3
window.require(['/api/plug/content/1'], function (m) {
  m.init($('#app-main')[0], args)
})

最后,之所以没有把页面全部改造为RequireJS,例如通过require载入主页面,主页面依赖react、公共组件库等,是因为我发现RequireJS的载入顺序与项目中使用的部分界面库有冲突,导致一些<a>的事件监听丢失(如下拉菜单不可用),根本原因还没找到。

原文地址:http://codemacro.com/2017/01/08/react-plugin/
written byKevin Lynx posted athttp://codemacro.com

]]>
0
<![CDATA[自动打Tag杂记]]> http://www.udpwork.com/item/16053.html http://www.udpwork.com/item/16053.html#reviews Sat, 07 Jan 2017 18:00:53 +0800 老王 http://www.udpwork.com/item/16053.html 给一段文字标记 Tag 是一个很常见的需求,比如我每篇博客下面都有对应的 Tag,不过一般说来,Tag 是数据录入者人为手动添加的,但是对大量用户产生的数据而言,我们不能指望他们能够主动添加合适的 Tag,于是乎就产生了这样的需求:自动打 Tag。

实际上这已经属于 NLP 高大上的范畴了,不是我这种非科班出身的人所能掌控的。好消息是百度腾讯都有 NLP 平台可供选择,坏消息是免费版的 API 配额极其有限。如果不差钱的话,直接选择 NLP 平台无疑是最方便的,不过对我来说还是基于开源软件自己搭建一套吧,常见选择有 THUTagJieba,因为 THUTag 是 Java 写的,Jieba 是 Python 写的,而我搞不定 Java,所以就选择了 Jieba。

如果要实现自动打 Tag,那么首先要实现分词,然后选择权重最大的词即可。

Jieba 是如何实现分词的呢?简单点说就是通过 trie 树扫描词典(dict.txt),然后基于词频找到最大切分组合,作者在issues里举例说明了实现过程,有兴趣可以看看。更神奇的是,在这个过程中,如果存在词典中没有的词,那么 Jieba 还可以根据 HMM 模型推断出可能的新词,不过这不是我们的重点,就不多说了。

在这里,一个重要的概念是词频(P),其在 Jieba 里的计算方法如下:

python> jieba.get_FREQ(...) / jieba.dt.total

下面通过「吴国忠臣伍子胥」这个例子来理解一下分词过程:

python> print("/".join(jieba.cut("吴国忠臣伍子胥")))
吴国忠/臣/伍子胥

显而易见,本次 Jieba 的分词是有问题的,为什么没有分词为「吴国/忠臣」呢?理解清楚这个问题,基本就能搞清楚 Jieba 是怎么分词的了:

python> from __future__ import unicode_literals

python> import jieba
python> jieba.initialize()

python> jieba.dt.total
60101967
python> jieba.get_FREQ("吴国忠")
14
python> jieba.get_FREQ("臣")
5666
python> jieba.get_FREQ("吴国")
174
python> jieba.get_FREQ("忠臣")
316

因为「P(吴国忠) * P(臣) > P(吴国) * P(忠臣)」,所以出现了错误的结果。此时我们可以通过调整相关词语的词频来解决问题,比如提升忠臣(忠臣啊!)的词频:

python> jieba.add_word("忠臣", 456)
python> print("/".join(jieba.cut("吴国忠臣伍子胥")))
吴国/忠臣/伍子胥

说明:456 是怎么来的?「14*5666/174 = 455.9」(本例可以省略 total)。

不过要实现自动打 Tag,光有分词还不够,我们还需要选择权重最大的词。Jieba 中有两种算法可供选择,它们分别是:TF-IDF 和 TextRank:

TF-IDF 指的是如果某个词在一篇文章中出现的频率高,并且在其他文章中出现频率不高,那么认为此词具有很好的类别区分能力,适合用来做关键词(当然,这个过程中要去掉通常无意义的停止词)。举例说明:现在各大门户网站的头版头条永远都是某大大的丰功伟绩,所以可以推定某大大从 TF-IDF 的角度看没有太大的价值。TextRank 则和 PageRank 基本是一个路子:临近的词语互相打分。

两种算法相比较,TF-IDF 要训练出一个idf.txt数据,而 TextRank 则不需要,看上去似乎 TextRank 更方便,但是从我的实际测试结果来看,对于短文本来说,如果有一个高质量的 idf 数据,那么 TF-IDF 的效果要比 TextRank 好得多。

如果你面对的是更专业化的语境,那么你可能想尝试构建自己的 idf.txt,操作前记得先倒入自定义的词典(userdict.txt),另外需要说明的是语料数据(data.txt)以行为单位:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from __future__ import print_function

import math
import sys
import jieba

jieba.load_userdict("userdict.txt")

data = {}
total = 0

with open('data.txt') as f:
    for line in f:
        line = line.decode("utf-8")
        words = [w for w in jieba.cut(line) if w in jieba.dt.FREQ]

        for word in words:
            data[word] = data.get(word, 0.0) + 1.0

        total += 1

        if total % 10000 == 0:
            print(total, file=sys.stderr)

data = [(k, math.log(total / v)) for k, v in data.iteritems()]

for k, v in data:
    print(k.encode("utf-8"), v)

如果需要抓取语料数据的话,推荐使用 requests + lxml,以百度的停止词为例:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from __future__ import print_function

import requests
from lxml import html

page = requests.get("http://www.baiduguide.com/baidu-stopwords/")
tree = html.fromstring(page.text)
elements = tree.xpath("//div/p[preceding-sibling::*[1][name()='h4']]/text()")

for element in elements:
    words = [e.strip() for e in element.split(",") if e.strip()]
    print(*words, sep="\n")

按照 xpath 来抓取数据,关键是抓取规则,推荐chrome xpath helper

XPath Helper

XPath Helper

最后看看我的成果吧,我大概收集了几百万条汽车维修方面的数据,然后通过 Jieba 自动给每条数据打 Tag,接着把得到的 Tag 以 Tag Cloud 的形式展示出来:

Tag Cloud

Tag Cloud

怎么样,通过 Jieba 自动的打 Tag,我们很清晰的可以看出在汽车维修领域,用户最容易遇到的问题是什么。嗯,估计有人会问这个 Tag Cloud 怎么画的,点这里。不谢!

]]>
给一段文字标记 Tag 是一个很常见的需求,比如我每篇博客下面都有对应的 Tag,不过一般说来,Tag 是数据录入者人为手动添加的,但是对大量用户产生的数据而言,我们不能指望他们能够主动添加合适的 Tag,于是乎就产生了这样的需求:自动打 Tag。

实际上这已经属于 NLP 高大上的范畴了,不是我这种非科班出身的人所能掌控的。好消息是百度腾讯都有 NLP 平台可供选择,坏消息是免费版的 API 配额极其有限。如果不差钱的话,直接选择 NLP 平台无疑是最方便的,不过对我来说还是基于开源软件自己搭建一套吧,常见选择有 THUTagJieba,因为 THUTag 是 Java 写的,Jieba 是 Python 写的,而我搞不定 Java,所以就选择了 Jieba。

如果要实现自动打 Tag,那么首先要实现分词,然后选择权重最大的词即可。

Jieba 是如何实现分词的呢?简单点说就是通过 trie 树扫描词典(dict.txt),然后基于词频找到最大切分组合,作者在issues里举例说明了实现过程,有兴趣可以看看。更神奇的是,在这个过程中,如果存在词典中没有的词,那么 Jieba 还可以根据 HMM 模型推断出可能的新词,不过这不是我们的重点,就不多说了。

在这里,一个重要的概念是词频(P),其在 Jieba 里的计算方法如下:

python> jieba.get_FREQ(...) / jieba.dt.total

下面通过「吴国忠臣伍子胥」这个例子来理解一下分词过程:

python> print("/".join(jieba.cut("吴国忠臣伍子胥")))
吴国忠/臣/伍子胥

显而易见,本次 Jieba 的分词是有问题的,为什么没有分词为「吴国/忠臣」呢?理解清楚这个问题,基本就能搞清楚 Jieba 是怎么分词的了:

python> from __future__ import unicode_literals

python> import jieba
python> jieba.initialize()

python> jieba.dt.total
60101967
python> jieba.get_FREQ("吴国忠")
14
python> jieba.get_FREQ("臣")
5666
python> jieba.get_FREQ("吴国")
174
python> jieba.get_FREQ("忠臣")
316

因为「P(吴国忠) * P(臣) > P(吴国) * P(忠臣)」,所以出现了错误的结果。此时我们可以通过调整相关词语的词频来解决问题,比如提升忠臣(忠臣啊!)的词频:

python> jieba.add_word("忠臣", 456)
python> print("/".join(jieba.cut("吴国忠臣伍子胥")))
吴国/忠臣/伍子胥

说明:456 是怎么来的?「14*5666/174 = 455.9」(本例可以省略 total)。

不过要实现自动打 Tag,光有分词还不够,我们还需要选择权重最大的词。Jieba 中有两种算法可供选择,它们分别是:TF-IDF 和 TextRank:

TF-IDF 指的是如果某个词在一篇文章中出现的频率高,并且在其他文章中出现频率不高,那么认为此词具有很好的类别区分能力,适合用来做关键词(当然,这个过程中要去掉通常无意义的停止词)。举例说明:现在各大门户网站的头版头条永远都是某大大的丰功伟绩,所以可以推定某大大从 TF-IDF 的角度看没有太大的价值。TextRank 则和 PageRank 基本是一个路子:临近的词语互相打分。

两种算法相比较,TF-IDF 要训练出一个idf.txt数据,而 TextRank 则不需要,看上去似乎 TextRank 更方便,但是从我的实际测试结果来看,对于短文本来说,如果有一个高质量的 idf 数据,那么 TF-IDF 的效果要比 TextRank 好得多。

如果你面对的是更专业化的语境,那么你可能想尝试构建自己的 idf.txt,操作前记得先倒入自定义的词典(userdict.txt),另外需要说明的是语料数据(data.txt)以行为单位:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from __future__ import print_function

import math
import sys
import jieba

jieba.load_userdict("userdict.txt")

data = {}
total = 0

with open('data.txt') as f:
    for line in f:
        line = line.decode("utf-8")
        words = [w for w in jieba.cut(line) if w in jieba.dt.FREQ]

        for word in words:
            data[word] = data.get(word, 0.0) + 1.0

        total += 1

        if total % 10000 == 0:
            print(total, file=sys.stderr)

data = [(k, math.log(total / v)) for k, v in data.iteritems()]

for k, v in data:
    print(k.encode("utf-8"), v)

如果需要抓取语料数据的话,推荐使用 requests + lxml,以百度的停止词为例:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from __future__ import print_function

import requests
from lxml import html

page = requests.get("http://www.baiduguide.com/baidu-stopwords/")
tree = html.fromstring(page.text)
elements = tree.xpath("//div/p[preceding-sibling::*[1][name()='h4']]/text()")

for element in elements:
    words = [e.strip() for e in element.split(",") if e.strip()]
    print(*words, sep="\n")

按照 xpath 来抓取数据,关键是抓取规则,推荐chrome xpath helper

XPath Helper

XPath Helper

最后看看我的成果吧,我大概收集了几百万条汽车维修方面的数据,然后通过 Jieba 自动给每条数据打 Tag,接着把得到的 Tag 以 Tag Cloud 的形式展示出来:

Tag Cloud

Tag Cloud

怎么样,通过 Jieba 自动的打 Tag,我们很清晰的可以看出在汽车维修领域,用户最容易遇到的问题是什么。嗯,估计有人会问这个 Tag Cloud 怎么画的,点这里。不谢!

]]>
0
<![CDATA[从 MongoDB “赎金事件” 看安全问题]]> http://www.udpwork.com/item/16052.html http://www.udpwork.com/item/16052.html#reviews Sat, 07 Jan 2017 17:11:28 +0800 陈皓 http://www.udpwork.com/item/16052.html 今天上午(2017年1月7日),我的微信群中同时出现了两个MongoDB被黑掉要赎金的情况,于是在调查过程中,发现了这个事件。这个事件应该是2017年开年的第一次比较大的安全事件吧,发现国内居然没有什么报道,国内安全圈也没有什么动静(当然,他们也许知道,只是不想说吧),Anyway,让我这个非安全领域的人来帮补补位。

事件回顾

这个事情应该是从2017年1月3日进入公众视野的,是由安全圈的大拿 Victor Gevers (网名:0xDUDEGDI.foundation 的Chairman),其实,他早在2016年12月27日就发现了一些在互联网上用户的MongoDB没有任何的保护措施,被攻击者把数据库删除了,并留下了一个叫 WARNING 的数据库,这张表的内容如下:

{
    "_id" : ObjectId("5859a0370b8e49f123fcc7da"),
    "mail" : "harak1r1@sigaint.org",
    "note" : "SEND 0.2 BTC TO THIS ADDRESS 13zaxGVjj9MNc2jyvDRhLyYpkCh323MsMq AND CONTACT THIS EMAIL WITH YOUR IP OF YOUR SERVER TO RECOVER YOUR DATABASE !"
}

基本上如下所示:

MongoDB ransom demand (via Victor Gevers)MongoDB ransom demand (via Victor Gevers)

说白了就是黑客留下的东西——老子把你的MongoDB里的数据库给转走了,如果你要你的数据的话,给我0.2个的比特币(大约USD200) 。然后,他的twitter上不断地发布这个“赎金事件”的跟踪报道。与此同时,中国区的V2EX上也发现了相关的攻击问题 《自己装的 mongo 没有设置密码结果被黑了

然后,在接下来的几天内,全球大约有1800个MongoDB的数据库被黑,这个行为来自一个叫 Harak1r1 的黑客组织(这个组织似乎就好黑MongoDB,据说他们历史上干了近8500个MongoDB的数据库,几乎都是在祼奔的MongoDB)。

不过,这个组织干了两天后就停手了,可能是因为这事已经引起了全球科技媒体的注意,产生了大量的报道(如果你在Google News里查一下“mongodb ransom”,你会看到大量的报道(中文社区中,只有台湾有相关的报道)),他们也许是不敢再搞下去了。

不过,很快,有几个copycats开始接着干,

马上跟进的是 own3d ,他们留下的数据库的名字叫 WARNING_ALERT,他们至少干掉了 930个MongoDB,赎金0.5个比特币(USD500),至少有3个用户付费了

然后是0704341626asdf,他们留下的数据库名字叫PWNED,他们至少干掉了740个MongoDB,赎金0.15个比特币(USD150),看看他们在数据库里留下的文字——你的MongoDB没有任何的认证,并且暴露在公网里(你TMD是怎么想的?)……

0704341626asdf group ransom note (via Victor Gerves)0704341626asdf group ransom note (via Victor Gerves)

就在这两天,有两个新的黑客也来了

  • 先是kraken0,发现到现在1天了,干了13个MongoDB,赎金 0.1个比特币。
  • 然后是 3lix1r,发现到现在5个小时,干了17个MongoDB,赎金0.25比特币。

BBC新闻也于昨天报道了这一情况——《Web databases hit in ransom attacks》,现在这个事情应该是一个Big News了。

关于MongoDB的安全

安全问题从来都是需要多方面一起努力,但是安全问题最大的短板就是在用户这边。这次的这个事,说白了,就是用户没有给MongoDB设置上用户名和口令,然后还把服务公开到了公网上。

是的,这个安全事件,相当的匪夷所思,为什么这些用户要在公网上祼奔自己的数据库?他们的脑子是怎么想的?

让我们去看一下Shodan上可以看到的有多少个在暴露在公网上而且没有防范的MongoDB?我了个去!4万7千个,还是很触目惊心的 (下图来自我刚刚创建的 Shodan关于MongoDB的报表

 

那么,怎么会有这么多的对外暴露的MongoDB?看了一下Shodan的报告,发现主要还是来自公有云平台,Amazon,Alibaba,Digital Ocean,OVH,Azure 的云平台上有很多这样的服务。不过,像AWS这样的云平台,有很完善的默认安全组设置和VPC是可以不把这样的后端服务暴露到公有云上的,为什么还会有那么多?

 

这么大量的暴露在公网上的服务是怎么回事?有人发现(参看这篇文章《It’s the Data, Stupid!》 ),MongoDB历史上一直都是把侦听端口绑在所有的IP上的,这个问题在5年前(2011年11月)就报给了MongoDB (SERVER-4216),结果2014年4月才解决掉。所以,他觉得可能似乎 MongoDB的 2.6之前的版本都会默认上侦听在0.0.0.0 。

于是我做了一个小试验,到我的Ubuntu 14.04上去 apt-get install mongodb(2.4.9版),然后我在/etc/mongodb.conf文件中,看到了默认的配置是127.0.0.1,mongod启动也侦听在了127.0.0.1这台机器上。一切正常。不过,可能是时过境迁,debain的安装包里已加上了这个默认配置文件。不管怎么样,MongoDB似乎是有一些问题的。

再到Shodan上看到相关的在公网裸奔的MongoDB的版本如下,发现3.x的也是主流:

 

虽然,3.x的版本成为了主流,但是似乎,还是有很多人把MongoDB的服务开到了互联网上来,而且可以随意访问。

你看,我在阿里云随便找了几台机器,一登就登上去了。

真是如那些黑客中的邮件所说的:WTF,你们是怎么想的?

后续的反思

为什么还是有这么多的MongoDB在公网上祼奔呢?难道有这么多的用户都是小白?这个原因,是什么呢?我觉得可能会是如下两个原因:

1)一是技术人员下载了mongod的软包,一般来说,mongodb的压缩包只有binary文件 ,没有配置文件 ,所以直接解开后运行,结果就没有安全认证,也绑在了公网上。也许,MongoDB这么做的原因就是为了可以快速上手,不要在环境上花太多的时间,这个有助于软件方面的推广。但是,这样可能就坑了更多的人。

2)因为MongoDB是后端基础服务,所以,需要很多内部机器防问,按道理呢,应该绑定在内网IP上,但是呢,可能是技术人员不小心,绑在了0.0.0.0的IP上。

那么,这个问题在云平台上是否可以更好的解决呢?

关于公网的IP。 一般来说,公有云平台上的虚拟主机都会有一个公网的IP地址,老实说,这并不是一个好的方法,因为有很多主机是不需要暴露到公网上的,所以,也就不需要使用公网IP,于是,就会出现弹性IP或虚拟路由器以及VPC这样的虚拟网络服务,这样用户在公有云就可以很容易的组网,也就没有必要每台机器都需要一个公网IP,使用云平台,最好还是使用组网方案比较好的平台。

关于安全组 。在AWS上,你开一台EC2,会有一个非常严格的安全组——只暴露22端口,其它的全部对外网关闭。这样做,其实是可以帮用户防止一下不小心把不必要的服务Open到公网上。按道理来说,AWS上应该是帮用户防了这些的。但是,AWS上的MongoDB祼奔的机器数量是最多的,估计和AWS的EC2的 基数有关系吧(据说AWS有千万台左右的EC2了)

最后,提醒大家一下,被黑了也不要去付赎金,因为目前来说没有任何证据证明黑客们真正保存了你的数据,因为,被黑的服务器太多了,估计有几百T的数据,估计是不会为你保存的。下面也是Victor Gevers的提示:

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处酷 壳 – CoolShell,请勿用于任何商业用途)

——===访问酷壳404页面寻找遗失儿童。===——
]]>
今天上午(2017年1月7日),我的微信群中同时出现了两个MongoDB被黑掉要赎金的情况,于是在调查过程中,发现了这个事件。这个事件应该是2017年开年的第一次比较大的安全事件吧,发现国内居然没有什么报道,国内安全圈也没有什么动静(当然,他们也许知道,只是不想说吧),Anyway,让我这个非安全领域的人来帮补补位。

事件回顾

这个事情应该是从2017年1月3日进入公众视野的,是由安全圈的大拿 Victor Gevers (网名:0xDUDEGDI.foundation 的Chairman),其实,他早在2016年12月27日就发现了一些在互联网上用户的MongoDB没有任何的保护措施,被攻击者把数据库删除了,并留下了一个叫 WARNING 的数据库,这张表的内容如下:

{
    "_id" : ObjectId("5859a0370b8e49f123fcc7da"),
    "mail" : "harak1r1@sigaint.org",
    "note" : "SEND 0.2 BTC TO THIS ADDRESS 13zaxGVjj9MNc2jyvDRhLyYpkCh323MsMq AND CONTACT THIS EMAIL WITH YOUR IP OF YOUR SERVER TO RECOVER YOUR DATABASE !"
}

基本上如下所示:

MongoDB ransom demand (via Victor Gevers)MongoDB ransom demand (via Victor Gevers)

说白了就是黑客留下的东西——老子把你的MongoDB里的数据库给转走了,如果你要你的数据的话,给我0.2个的比特币(大约USD200) 。然后,他的twitter上不断地发布这个“赎金事件”的跟踪报道。与此同时,中国区的V2EX上也发现了相关的攻击问题 《自己装的 mongo 没有设置密码结果被黑了

然后,在接下来的几天内,全球大约有1800个MongoDB的数据库被黑,这个行为来自一个叫 Harak1r1 的黑客组织(这个组织似乎就好黑MongoDB,据说他们历史上干了近8500个MongoDB的数据库,几乎都是在祼奔的MongoDB)。

不过,这个组织干了两天后就停手了,可能是因为这事已经引起了全球科技媒体的注意,产生了大量的报道(如果你在Google News里查一下“mongodb ransom”,你会看到大量的报道(中文社区中,只有台湾有相关的报道)),他们也许是不敢再搞下去了。

不过,很快,有几个copycats开始接着干,

马上跟进的是 own3d ,他们留下的数据库的名字叫 WARNING_ALERT,他们至少干掉了 930个MongoDB,赎金0.5个比特币(USD500),至少有3个用户付费了

然后是0704341626asdf,他们留下的数据库名字叫PWNED,他们至少干掉了740个MongoDB,赎金0.15个比特币(USD150),看看他们在数据库里留下的文字——你的MongoDB没有任何的认证,并且暴露在公网里(你TMD是怎么想的?)……

0704341626asdf group ransom note (via Victor Gerves)0704341626asdf group ransom note (via Victor Gerves)

就在这两天,有两个新的黑客也来了

  • 先是kraken0,发现到现在1天了,干了13个MongoDB,赎金 0.1个比特币。
  • 然后是 3lix1r,发现到现在5个小时,干了17个MongoDB,赎金0.25比特币。

BBC新闻也于昨天报道了这一情况——《Web databases hit in ransom attacks》,现在这个事情应该是一个Big News了。

关于MongoDB的安全

安全问题从来都是需要多方面一起努力,但是安全问题最大的短板就是在用户这边。这次的这个事,说白了,就是用户没有给MongoDB设置上用户名和口令,然后还把服务公开到了公网上。

是的,这个安全事件,相当的匪夷所思,为什么这些用户要在公网上祼奔自己的数据库?他们的脑子是怎么想的?

让我们去看一下Shodan上可以看到的有多少个在暴露在公网上而且没有防范的MongoDB?我了个去!4万7千个,还是很触目惊心的 (下图来自我刚刚创建的 Shodan关于MongoDB的报表

 

那么,怎么会有这么多的对外暴露的MongoDB?看了一下Shodan的报告,发现主要还是来自公有云平台,Amazon,Alibaba,Digital Ocean,OVH,Azure 的云平台上有很多这样的服务。不过,像AWS这样的云平台,有很完善的默认安全组设置和VPC是可以不把这样的后端服务暴露到公有云上的,为什么还会有那么多?

 

这么大量的暴露在公网上的服务是怎么回事?有人发现(参看这篇文章《It’s the Data, Stupid!》 ),MongoDB历史上一直都是把侦听端口绑在所有的IP上的,这个问题在5年前(2011年11月)就报给了MongoDB (SERVER-4216),结果2014年4月才解决掉。所以,他觉得可能似乎 MongoDB的 2.6之前的版本都会默认上侦听在0.0.0.0 。

于是我做了一个小试验,到我的Ubuntu 14.04上去 apt-get install mongodb(2.4.9版),然后我在/etc/mongodb.conf文件中,看到了默认的配置是127.0.0.1,mongod启动也侦听在了127.0.0.1这台机器上。一切正常。不过,可能是时过境迁,debain的安装包里已加上了这个默认配置文件。不管怎么样,MongoDB似乎是有一些问题的。

再到Shodan上看到相关的在公网裸奔的MongoDB的版本如下,发现3.x的也是主流:

 

虽然,3.x的版本成为了主流,但是似乎,还是有很多人把MongoDB的服务开到了互联网上来,而且可以随意访问。

你看,我在阿里云随便找了几台机器,一登就登上去了。

真是如那些黑客中的邮件所说的:WTF,你们是怎么想的?

后续的反思

为什么还是有这么多的MongoDB在公网上祼奔呢?难道有这么多的用户都是小白?这个原因,是什么呢?我觉得可能会是如下两个原因:

1)一是技术人员下载了mongod的软包,一般来说,mongodb的压缩包只有binary文件 ,没有配置文件 ,所以直接解开后运行,结果就没有安全认证,也绑在了公网上。也许,MongoDB这么做的原因就是为了可以快速上手,不要在环境上花太多的时间,这个有助于软件方面的推广。但是,这样可能就坑了更多的人。

2)因为MongoDB是后端基础服务,所以,需要很多内部机器防问,按道理呢,应该绑定在内网IP上,但是呢,可能是技术人员不小心,绑在了0.0.0.0的IP上。

那么,这个问题在云平台上是否可以更好的解决呢?

关于公网的IP。 一般来说,公有云平台上的虚拟主机都会有一个公网的IP地址,老实说,这并不是一个好的方法,因为有很多主机是不需要暴露到公网上的,所以,也就不需要使用公网IP,于是,就会出现弹性IP或虚拟路由器以及VPC这样的虚拟网络服务,这样用户在公有云就可以很容易的组网,也就没有必要每台机器都需要一个公网IP,使用云平台,最好还是使用组网方案比较好的平台。

关于安全组 。在AWS上,你开一台EC2,会有一个非常严格的安全组——只暴露22端口,其它的全部对外网关闭。这样做,其实是可以帮用户防止一下不小心把不必要的服务Open到公网上。按道理来说,AWS上应该是帮用户防了这些的。但是,AWS上的MongoDB祼奔的机器数量是最多的,估计和AWS的EC2的 基数有关系吧(据说AWS有千万台左右的EC2了)

最后,提醒大家一下,被黑了也不要去付赎金,因为目前来说没有任何证据证明黑客们真正保存了你的数据,因为,被黑的服务器太多了,估计有几百T的数据,估计是不会为你保存的。下面也是Victor Gevers的提示:

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处酷 壳 – CoolShell,请勿用于任何商业用途)

——===访问酷壳404页面寻找遗失儿童。===——
]]>
0
<![CDATA[2017内容创业:商业迭代]]> http://www.udpwork.com/item/16051.html http://www.udpwork.com/item/16051.html#reviews Sat, 07 Jan 2017 01:15:40 +0800 魏武挥 http://www.udpwork.com/item/16051.html 写在前面的话:限于篇幅,此文没有展开。最近我打算边肝边写个系列。

“内容创业”已成为一个站得住的概念。

与很多创业概念——比如O2O——所不同,内容创业一般一开始就有现金流。现金流状况非常好的内容创业项目,甚至不需要任何资本注入。

它本身就是一个能成立的生意模式。

当然,也有不少内容创业项目取得了大额的融资,并开始迈向除却广告模式之外的商业模式。

我将其称为:商业迭代。

 

去年年底,一个名为四番群的微信群,攒了一个年会。

群中有一位有“互联网女侠”之称的群友,现在正在捣鼓一些天使投资的梁宁,提到了媒体的三大价值:广告价值、公关价值和连接价值。

媒体具有广告价值是很容易理解的事。

媒体具有公关价值也是很容易理解的事,虽然软文这件事,在我看来,从来都是上不得台面的事。

连接价值,稍微解释一下。

有一些媒体,由于它的内容关系,经常会和一些大佬接触。这种媒体就是大佬们中的一个桥节点。在合适的时候,它能爆发出很强的力量。

一家已经申报创业板上市的媒体,它的创始人,这个桥节点,给他以很大的帮助。

公号生态里,其实颇有这样的内容创业项目,有的甚至已经估值过亿。

不过,在我看来,在梁宁的连接价值基础之上,媒体将会蔓延出一个全新的价值。

交易价值。

交易价值建立在大规模的连接之上。而不是前面所提到的大佬的桥节点,那个从人数规模而言,并不大。

 

请允许我稍微岔开去一点。

曹政在他的公号“caoz的梦呓”中写过一篇题为“知识分享,红利期还有多久”。

中间有一段话:

我们那一代,70后,跟我们父母那一代,消费观截然不同,80后比70后好一些,90后和我们比,完全是不同时代的人,消费观差异相当巨大。那么我强调一点,最近几年,互联网最大红利是,年轻一代的消费观差异!

这段话我有所感悟,但这段话并没有说明:到底是什么消费观差异。70后的我,和90后的我的学生,消费时究竟有什么不同?

经过一段时间的琢磨,我想明白了这里的差异。

年纪稍长的人,他们的消费观念是:赶集。

年纪很轻的一代,他们的消费观念是:随需随买。

可能一个年纪很轻的人,不一定会购买中年人的东西,但当未来ta进入中年后,同样会购买。但ta的消费方法论和ta的前辈并不同。

 

我是一个七零后生人。

在我的记忆深处,存在过“饥饿”二字。毕竟,真正迎来供给大富裕的时代,是八十年代以后的事。

我一度有“凑单”的习惯,比如在某电子商务网站上,一单超过99的购买金额可以免运费,我会攒着这个货物凑单购买。

我第一台电脑的硬盘只有几十个MB,这养成了我没事就看看还有多少空间的习惯。前几年我第一次开始使用MacBook时,非常困惑地问别人:“我的电脑”在哪里?我需要知道自己硬盘空间还有多大。

但今天一二线城市的年轻人,恐怕饥饿二字,和他们并没有什么关系。

你可以批评他们“冲动消费”、“不理性消费”。

但你不得不承认一个事实,随需随买,事实存在。

而且,我相信一件事,那就是即便等他们人到中年,他们的消费观念依然是:随需随买。

这种骨子里的方法论,是很难改变的。

 

前互联网时代,人们的消费其实是一趟赶集,比如周末一起去逛商场——请注意这句话,两个要素:时间、地点是中心化的。

互联网时代,还是赶集。互联网解构掉了时间,7*24小时都可以消费,但交易场所依然是集中的。比如淘宝,比如京东。这种集中体现在,你登录网站的首页,开始“逛”淘宝、“逛”京东。

设想这样一种场景。

某媒体文章介绍一种东西很不错。天花乱坠之余,它只是提到了这种商品的名字,并没有购买指导。

这会让年轻人很不耐烦。

我已经多次看到这样的评论:讲了半天不给购买链接,你逗我啊?

这和过去的媒体时代是何等不同。

传统媒体在介绍一个产品时,还生怕读者以为是媒体的软文,有失操守。

但在今天,你不赤裸裸地放出购买链接,反而会被你的粉丝、用户唾骂。

从一个内容出发,引起购买兴趣,其实是消费的去中心化。

时间已经去中心,购买都在去中心。

即便商品仍在淘宝页面上,但消费者并不是从首页进入进行一番搜索或闲逛,而是直达商品页。

有些消费者甚至会认为,这就是这个内容背后的运营者在售货,淘宝也好京东也好,与ta本人无关。

 

无论是广告价值,还是公关价值,这是一个三方的商业模式。

媒体将其用户(读者)作为一种注意力,转卖给第三方,也就是甲方。

但交易价值,是一个两方的商业模式,用户即甲方。

交易价值建立在一种连接之上,就算是具象的形式,都需要购买链接。

不得不说,广告价值的毛利率高,交易价值的毛利率低(如果是实物的话)。

但有趣的事是,依托高毛利率的广告的媒体,从来不是什么规模庞大的生意。纽约时报全球知名,但从商业角度看,它不是一个大公司。但依托低毛利率的卖货的企业,无论是沃尔玛,还是京东,都是市值巨大的主。

但凡有勃勃雄心的内容创业者,一定会不仅仅局限于广告模式。

移动互联网带来的交易便捷,代际人口交替带来的观念变化,为最终达成去中心化消费的内容通道,建立了可靠的基石。

广告价值依然存在,交易价值将在2017年规模化的得以凸现。

是为商业迭代。

 

过去的2016年,总让我感到这样一件事:内容创业在广告模式上的战斗,大体已经结束。

未来的广告投放,主要集中于两个地方:其一,渠道。比如今日头条,或者腾讯的效果广告。当然,还有一些其它内容渠道也能分到一杯羹。其二,头部大号。这些头部大号,动辄大几百万粉丝,与甲方有极强的议价能力,部分甚至可以雇佣成体系的销售团队。

腰部的内容创业项目,广告上不是不能分到,而是说,基本属于等人吃完了肉去喝点汤。

影响力在腰部的,通常都是垂直性质的,无论是细分垂直,还是地域垂直。否则,做不到头部,属于能力极其有限。

垂直内容创业项目,必须着眼于交易价值,这是应战头部大号的唯一机会。

即将展开的2017年,是内容创业市场上的洗牌年。所有的内容创业,彼此都是竞争关系。用户大盘已经不再有飙升式的增长,纯人口统计口径上的红利已经耗尽。

有人曾经在一台电视节目中提到,内容创业必须精耕细作。

在场的我,咄咄逼人地问她:什么叫精耕细作?

其实我是有答案的。

那就是挖掘交易价值。

 

让我再一次重复caoz的这句话:

最近几年,互联网最大红利是,年轻一代的消费观差异!

请千万不要忘记,他们“随需随买”的重大特征。

 

—— 首发 插坐学院 ——

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

2017内容创业:商业迭代,首发于扯氮集

]]>
写在前面的话:限于篇幅,此文没有展开。最近我打算边肝边写个系列。

“内容创业”已成为一个站得住的概念。

与很多创业概念——比如O2O——所不同,内容创业一般一开始就有现金流。现金流状况非常好的内容创业项目,甚至不需要任何资本注入。

它本身就是一个能成立的生意模式。

当然,也有不少内容创业项目取得了大额的融资,并开始迈向除却广告模式之外的商业模式。

我将其称为:商业迭代。

 

去年年底,一个名为四番群的微信群,攒了一个年会。

群中有一位有“互联网女侠”之称的群友,现在正在捣鼓一些天使投资的梁宁,提到了媒体的三大价值:广告价值、公关价值和连接价值。

媒体具有广告价值是很容易理解的事。

媒体具有公关价值也是很容易理解的事,虽然软文这件事,在我看来,从来都是上不得台面的事。

连接价值,稍微解释一下。

有一些媒体,由于它的内容关系,经常会和一些大佬接触。这种媒体就是大佬们中的一个桥节点。在合适的时候,它能爆发出很强的力量。

一家已经申报创业板上市的媒体,它的创始人,这个桥节点,给他以很大的帮助。

公号生态里,其实颇有这样的内容创业项目,有的甚至已经估值过亿。

不过,在我看来,在梁宁的连接价值基础之上,媒体将会蔓延出一个全新的价值。

交易价值。

交易价值建立在大规模的连接之上。而不是前面所提到的大佬的桥节点,那个从人数规模而言,并不大。

 

请允许我稍微岔开去一点。

曹政在他的公号“caoz的梦呓”中写过一篇题为“知识分享,红利期还有多久”。

中间有一段话:

我们那一代,70后,跟我们父母那一代,消费观截然不同,80后比70后好一些,90后和我们比,完全是不同时代的人,消费观差异相当巨大。那么我强调一点,最近几年,互联网最大红利是,年轻一代的消费观差异!

这段话我有所感悟,但这段话并没有说明:到底是什么消费观差异。70后的我,和90后的我的学生,消费时究竟有什么不同?

经过一段时间的琢磨,我想明白了这里的差异。

年纪稍长的人,他们的消费观念是:赶集。

年纪很轻的一代,他们的消费观念是:随需随买。

可能一个年纪很轻的人,不一定会购买中年人的东西,但当未来ta进入中年后,同样会购买。但ta的消费方法论和ta的前辈并不同。

 

我是一个七零后生人。

在我的记忆深处,存在过“饥饿”二字。毕竟,真正迎来供给大富裕的时代,是八十年代以后的事。

我一度有“凑单”的习惯,比如在某电子商务网站上,一单超过99的购买金额可以免运费,我会攒着这个货物凑单购买。

我第一台电脑的硬盘只有几十个MB,这养成了我没事就看看还有多少空间的习惯。前几年我第一次开始使用MacBook时,非常困惑地问别人:“我的电脑”在哪里?我需要知道自己硬盘空间还有多大。

但今天一二线城市的年轻人,恐怕饥饿二字,和他们并没有什么关系。

你可以批评他们“冲动消费”、“不理性消费”。

但你不得不承认一个事实,随需随买,事实存在。

而且,我相信一件事,那就是即便等他们人到中年,他们的消费观念依然是:随需随买。

这种骨子里的方法论,是很难改变的。

 

前互联网时代,人们的消费其实是一趟赶集,比如周末一起去逛商场——请注意这句话,两个要素:时间、地点是中心化的。

互联网时代,还是赶集。互联网解构掉了时间,7*24小时都可以消费,但交易场所依然是集中的。比如淘宝,比如京东。这种集中体现在,你登录网站的首页,开始“逛”淘宝、“逛”京东。

设想这样一种场景。

某媒体文章介绍一种东西很不错。天花乱坠之余,它只是提到了这种商品的名字,并没有购买指导。

这会让年轻人很不耐烦。

我已经多次看到这样的评论:讲了半天不给购买链接,你逗我啊?

这和过去的媒体时代是何等不同。

传统媒体在介绍一个产品时,还生怕读者以为是媒体的软文,有失操守。

但在今天,你不赤裸裸地放出购买链接,反而会被你的粉丝、用户唾骂。

从一个内容出发,引起购买兴趣,其实是消费的去中心化。

时间已经去中心,购买都在去中心。

即便商品仍在淘宝页面上,但消费者并不是从首页进入进行一番搜索或闲逛,而是直达商品页。

有些消费者甚至会认为,这就是这个内容背后的运营者在售货,淘宝也好京东也好,与ta本人无关。

 

无论是广告价值,还是公关价值,这是一个三方的商业模式。

媒体将其用户(读者)作为一种注意力,转卖给第三方,也就是甲方。

但交易价值,是一个两方的商业模式,用户即甲方。

交易价值建立在一种连接之上,就算是具象的形式,都需要购买链接。

不得不说,广告价值的毛利率高,交易价值的毛利率低(如果是实物的话)。

但有趣的事是,依托高毛利率的广告的媒体,从来不是什么规模庞大的生意。纽约时报全球知名,但从商业角度看,它不是一个大公司。但依托低毛利率的卖货的企业,无论是沃尔玛,还是京东,都是市值巨大的主。

但凡有勃勃雄心的内容创业者,一定会不仅仅局限于广告模式。

移动互联网带来的交易便捷,代际人口交替带来的观念变化,为最终达成去中心化消费的内容通道,建立了可靠的基石。

广告价值依然存在,交易价值将在2017年规模化的得以凸现。

是为商业迭代。

 

过去的2016年,总让我感到这样一件事:内容创业在广告模式上的战斗,大体已经结束。

未来的广告投放,主要集中于两个地方:其一,渠道。比如今日头条,或者腾讯的效果广告。当然,还有一些其它内容渠道也能分到一杯羹。其二,头部大号。这些头部大号,动辄大几百万粉丝,与甲方有极强的议价能力,部分甚至可以雇佣成体系的销售团队。

腰部的内容创业项目,广告上不是不能分到,而是说,基本属于等人吃完了肉去喝点汤。

影响力在腰部的,通常都是垂直性质的,无论是细分垂直,还是地域垂直。否则,做不到头部,属于能力极其有限。

垂直内容创业项目,必须着眼于交易价值,这是应战头部大号的唯一机会。

即将展开的2017年,是内容创业市场上的洗牌年。所有的内容创业,彼此都是竞争关系。用户大盘已经不再有飙升式的增长,纯人口统计口径上的红利已经耗尽。

有人曾经在一台电视节目中提到,内容创业必须精耕细作。

在场的我,咄咄逼人地问她:什么叫精耕细作?

其实我是有答案的。

那就是挖掘交易价值。

 

让我再一次重复caoz的这句话:

最近几年,互联网最大红利是,年轻一代的消费观差异!

请千万不要忘记,他们“随需随买”的重大特征。

 

—— 首发 插坐学院 ——

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

2017内容创业:商业迭代,首发于扯氮集

]]>
0
<![CDATA[[]T 还是 []*T, 这是一个问题]]> http://www.udpwork.com/item/16050.html http://www.udpwork.com/item/16050.html#reviews Thu, 05 Jan 2017 18:49:26 +0800 鸟窝 http://www.udpwork.com/item/16050.html 在编程语言深入讨论中,经常被大家提起也是争论最多的讨论之一就是按值(by value)还是按引用传递(by reference, by pointer),你可以在C/C++或者Java的社区经常看到这样的讨论,也会看到很多这样的面试题。

对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。

当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。

这是Go语言中你理解后续问题的基础。

但是Go语言的情况比较复杂,我们什么时候选择T作为参数类型,什么时候选择*T作为参数类型?[]T是传递的指针还是值?选择[]T还是[]*T? 哪些类型复制和传递的时候会创建副本?什么情况下会发生副本创建?

本文将详细介绍Go语言的变量的副本创建还是变量指针的副本创建的case以及各种类型在这些case的情况。

副本的创建

前面已经讲到,T类型的变量和*T类型的变量在当做函数或者方法的参数时会传递它的副本。我们先看看例子。

T的副本创建

首先看一下 参数类型为T的函数调用的情况:

123456789101112131415161718192021
package mainimport "fmt"type Bird struct {	Age  int	Name string}func passV(b Bird) {	b.Age++	b.Name = "Great" + b.Name	fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p\n", b, &b)}func main() {	parrot := Bird{Age: 1, Name: "Blue"}	fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", parrot, &parrot)	passV(parrot)	fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", parrot, &parrot)}

运行后输入结果(每次运行指针的值可能不同):

123
原始的Bird:		 {Age:1 Name:Blue}, 		内存地址:0xc420012260传入修改后的Bird:	 {Age:2 Name:GreatBlue}, 	内存地址:0xc4200122c0调用后原始的Bird:	 {Age:1 Name:Blue}, 		内存地址:0xc420012260

可以看到,在T类型作为参数的时候,传递的参数parrot会将它的副本(内存地址0xc4200122c0)传递给函数passV,在这个函数内对参数的改变不会影响原始的对象。

*T的副本创建

修改上面的例子,将函数的参数类型由T改为*T:

123456789101112131415161718192021
package mainimport "fmt"type Bird struct {	Age  int	Name string}func passP(b *Bird) {	b.Age++	b.Name = "Great" + b.Name	fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *b, b, &b)}func main() {	parrot := &Bird{Age: 1, Name: "Blue"}	fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)	passP(parrot)	fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)}

运行后输出结果:

123
原始的Bird:		 {Age:1 Name:Blue}, 		内存地址:0xc420076000, 指针的内存地址: 0xc420074000传入修改后的Bird:	 {Age:2 Name:GreatBlue}, 	内存地址:0xc420076000, 指针的内存地址: 0xc420074010调用后原始的Bird:	 {Age:2 Name:GreatBlue}, 	内存地址:0xc420076000, 指针的内存地址: 0xc420074000

可以看到在函数passP中,参数p是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc420074000和0xc420074010都指向内存地址0xc420076000。 函数内对*T的改变显然会影响原始的对象,因为它是对同一个对象的操作。

当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T和*T作为参数的时候副本创建的不同。

如何选择T和*T

在定义函数和方法的时候,作为一位资深的Go开发人员,一定会对函数的参数和返回值定义成T和*T深思熟虑,有些情况下可能还会有些苦恼。
那么什么时候才应该把参数定义成类型T,什么情况下定义成类型*T呢。

一般的判断标准是看副本创建的成本和需求。

  1. 不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型T。相反,如果想修改原始的变量,则选择*T
  2. 如果变量是一个 的struct或者数组,则副本的创建相对会影响性能,这个时候考虑使用*T,只创建新的指针,这个区别是巨大的
  3. (不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成T,Go编译器尽量将对象分配到栈上,而*T很可能会分配到对象上,这对垃圾回收会有影响

什么时候发生副本创建

上面举的例子都是作为函数参数时发生的副本的创建,还有很多情况下会发生副本的创建,甚至有些“隐蔽”的情况。
编程的时候如何小心这些情况呢,一条原则就是:

A go assignment is a copy of the value itself
赋值的时候就会创建对象副本

Assignment的语法表达式如下:

Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .

Each left-hand side operand must be addressable, a map index expression, or (for = assignments only) the blank identifier. Operands may be parenthesized.

最常见的case

最常见的赋值的例子是对变量的赋值,包括函数内和函数外:

123456789101112131415161718192021222324252627
package mainimport "fmt"type Bird struct {	Age  int	Name string}type Parrot struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}var parrot2 = parrot1func main() {	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot2, &parrot2)	parrot3 := parrot1	fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot3, &parrot3)	parrot4 := Parrot(parrot1)	fmt.Printf("parrot4:\t\t %+v, \t\t内存地址:%p\n", parrot4, &parrot4)}

输出结果:

1234
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfa0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xfa0c0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc42007e0c0parrot4:		 {Age:1 Name:Blue}, 		内存地址:0xc42007e100

可以看到这几个变量的内存地址都不相同,说明发生了赋值。

map、slice和数组

slice,map和数组在初始化和按索引设置的时候也会创建副本:

1234567891011121314151617181920212223242526272829303132333435363738394041
package mainimport "fmt"type Bird struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}func main() {	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	//slice	s := []Bird{parrot1}	s = append(s, parrot1)	parrot1.Age = 3	fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", s[0], &(s[0]))	fmt.Printf("parrot3:\t\t %+v, \t\t内存地址:%p\n", s[1], &(s[1]))	parrot1.Age = 1	//map	m := make(map[int]Bird)	m[0] = parrot1	parrot1.Age = 4	fmt.Printf("parrot4:\t\t %+v\n", m[0])	parrot1.Age = 5	parrot5 := m[0]	fmt.Printf("parrot5:\t\t %+v, \t\t内存地址:%p\n", parrot5, &parrot5)	parrot1.Age = 1	//array	a := [2]Bird{parrot1}	parrot1.Age = 6	fmt.Printf("parrot6:\t\t %+v, \t\t内存地址:%p\n", a[0], &a[0])	parrot1.Age = 1	a[1] = parrot1	parrot1.Age = 7	fmt.Printf("parrot7:\t\t %+v, \t\t内存地址:%p\n", a[1], &a[1])}

输出结果

1234567
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfa0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200160f0parrot3:		 {Age:1 Name:Blue}, 		内存地址:0xc420016108parrot4:		 {Age:1 Name:Blue}parrot5:		 {Age:1 Name:Blue}, 		内存地址:0xc420012320parrot6:		 {Age:1 Name:Blue}, 		内存地址:0xc420016120parrot7:		 {Age:1 Name:Blue}, 		内存地址:0xc420016138

可以看到 slice/map/数组 的元素全是原始变量的副本,副本

for-range循环

for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
package mainimport "fmt"type Bird struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}func main() {	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	//slice	s := []Bird{parrot1, parrot1, parrot1}	s[0].Age = 1	s[1].Age = 2	s[2].Age = 3	parrot1.Age = 4	for i, p := range s {		fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i + 2), p, &p)	}	parrot1.Age = 1	//map	m := make(map[int]Bird)	parrot1.Age = 1	m[0] = parrot1	parrot1.Age = 2	m[1] = parrot1	parrot1.Age = 3	m[2] = parrot1	parrot1.Age = 4	for k, v := range m {		fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (k + 2), v, &v)	}	parrot1.Age = 4	//array	a := [...]Bird{parrot1, parrot1, parrot1}	a[0].Age = 1	a[1].Age = 2	a[2].Age = 3	parrot1.Age = 4	for i, p := range a {		fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i + 2), p, &p)	}}

输出结果

12345678910
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfb0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200122a0parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc4200122a0parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc4200122a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc420012320parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc420012320parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc420012320parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200123a0parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc4200123a0parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc4200123a0

注意循环变量是重用的,所以你看到它们的地址是相同的。

channel

往channel中send对象的时候也会创建对象的副本:

12345678910111213141516171819202122232425262728
package mainimport "fmt"type Bird struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}func main() {	ch := make(chan Bird, 3)	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	ch <- parrot1	parrot1.Age = 2	ch <- parrot1	parrot1.Age = 3	ch <- parrot1	parrot1.Age = 4	p := <-ch	fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", 2, p, &p)	p = <-ch	fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", 3, p, &p)	p = <-ch	fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", 4, p, &p)}

输出结果:

1234
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfa0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200122a0parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc4200122a0parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc4200122a0

函数参数和返回值

将变量作为参数传递给函数和方法会发生副本的创建。
对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本。

Method Receiver

因为方法(method)最终会产生一个receiver作为第一个参数的函数(参看规范),所以就比较好理解method receiver的副本创建的规则了。
当receiver为T类型时,会发生创建副本,调用副本上的方法。
当receiver为*T类型时,只是会创建对象的指针,不创建对象的副本,方法内对receiver的改动会影响原始值。

不同类型的副本创建

bool,数值和指针

bool和数值类型一般不必考虑指针类型,原因在于这些对象很小,创建副本的开销可以忽略。只有你在想修改同一个变量的值的时候才考虑它们的指针。

指针类型就不用多说了,和数值类型类似。

数组

数组是值类型,赋值的时候会发生原始数组的复制,所以对于大的数组的参数传递和赋值,一定要慎重。

12345678910111213
package mainimport "fmt"func main() {	a1 := [3]int{1, 2, 3}	fmt.Printf("a1:\t\t %+v, \t\t内存地址:%p\n", a1, &a1)	a2 := a1	a1[0] = 4	a1[1] = 5	a1[2] = 6	fmt.Printf("a2:\t\t %+v, \t\t内存地址:%p\n", a2, &a2)}

输出

12
a1:		 [1 2 3], 		内存地址:0xc420012260a2:		 [1 2 3], 		内存地址:0xc4200122c0

对于[...]T和[...]*T的区别,我想你也应该清楚了,[...]*T创建的副本的元素时元数组元素指针的副本。

map、slice 和 channel

网上一般说, 这三种类型都是指向指针类型,指向一个底层的数据结构。
因此呢,在定义类型的时候就不必定义成*T了。

当然你可以这么认为,不过我认为这是不准确的,比如slice,其实你可以看成是SliceHeader对象,只不过它的数据Data是一个指针,所以它的副本的创建对性能的影响可以忽略。

字符串

string类型类似slice,它等价StringHeader。所以很多情况下会用`unsafe.Pointer`与[]byte类型进行更有效的转换,因为直接进行类型转换string([]byte)会发生数据的复制。

字符串比较特殊,它的值不能修改,任何想对字符串的值做修改都会生成新的字符串。

大部分情况下你不需要定义成*string。唯一的例外你需要nil值的时候。我们知道,类型string的空值/缺省值为"",但是如果你需要nil,你就必须定义*string。举个例子,在对象序列化的时候""和nil表示的意义是不一样的,""表示字段存在,只不过字符串是空值,而nil表示字段不存在。

函数

函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。

1234567891011
package mainimport "fmt"func main() {	f1 := func(i int) {}	fmt.Printf("f1:\t\t %+v, \t\t内存地址:%p\n", f1, &f1)	f2 := f1	fmt.Printf("f2:\t\t %+v, \t\t内存地址:%p\n", f2, &f2)}

输出结果:

12
f1:		 0x2200, 		内存地址:0xc420028020f2:		 0x2200, 		内存地址:0xc420028030

参考文档

  1. https://www.reddit.com/r/golang/comments/5lheyg/returning_t_vs_t/?
  2. https://github.com/google/go-github/issues/180
  3. http://openmymind.net/Things-I-Wish-Someone-Had-Told-Me-About-Go/
  4. http://goinbigdata.com/golang-pass-by-pointer-vs-pass-by-value/
  5. https://groups.google.com/forum/#!topic/golang-nuts/__BPVgK8LN0
  6. https://golang.org/ref/spec
  7. https://golang.org/doc/faq
  8. https://golang.org/doc/effective_go.html
  9. https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/
  10. https://dhdersch.github.io/golang/2016/01/23/golang-when-to-use-string-pointers.html
  11. https://dave.cheney.net/2016/03/19/should-methods-be-declared-on-t-or-t
  12. http://colobu.com/2016/10/28/When-are-function-parameters-passed-by-value/
]]>
在编程语言深入讨论中,经常被大家提起也是争论最多的讨论之一就是按值(by value)还是按引用传递(by reference, by pointer),你可以在C/C++或者Java的社区经常看到这样的讨论,也会看到很多这样的面试题。

对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。

当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。

这是Go语言中你理解后续问题的基础。

但是Go语言的情况比较复杂,我们什么时候选择T作为参数类型,什么时候选择*T作为参数类型?[]T是传递的指针还是值?选择[]T还是[]*T? 哪些类型复制和传递的时候会创建副本?什么情况下会发生副本创建?

本文将详细介绍Go语言的变量的副本创建还是变量指针的副本创建的case以及各种类型在这些case的情况。

副本的创建

前面已经讲到,T类型的变量和*T类型的变量在当做函数或者方法的参数时会传递它的副本。我们先看看例子。

T的副本创建

首先看一下 参数类型为T的函数调用的情况:

123456789101112131415161718192021
package mainimport "fmt"type Bird struct {	Age  int	Name string}func passV(b Bird) {	b.Age++	b.Name = "Great" + b.Name	fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p\n", b, &b)}func main() {	parrot := Bird{Age: 1, Name: "Blue"}	fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", parrot, &parrot)	passV(parrot)	fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", parrot, &parrot)}

运行后输入结果(每次运行指针的值可能不同):

123
原始的Bird:		 {Age:1 Name:Blue}, 		内存地址:0xc420012260传入修改后的Bird:	 {Age:2 Name:GreatBlue}, 	内存地址:0xc4200122c0调用后原始的Bird:	 {Age:1 Name:Blue}, 		内存地址:0xc420012260

可以看到,在T类型作为参数的时候,传递的参数parrot会将它的副本(内存地址0xc4200122c0)传递给函数passV,在这个函数内对参数的改变不会影响原始的对象。

*T的副本创建

修改上面的例子,将函数的参数类型由T改为*T:

123456789101112131415161718192021
package mainimport "fmt"type Bird struct {	Age  int	Name string}func passP(b *Bird) {	b.Age++	b.Name = "Great" + b.Name	fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *b, b, &b)}func main() {	parrot := &Bird{Age: 1, Name: "Blue"}	fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)	passP(parrot)	fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)}

运行后输出结果:

123
原始的Bird:		 {Age:1 Name:Blue}, 		内存地址:0xc420076000, 指针的内存地址: 0xc420074000传入修改后的Bird:	 {Age:2 Name:GreatBlue}, 	内存地址:0xc420076000, 指针的内存地址: 0xc420074010调用后原始的Bird:	 {Age:2 Name:GreatBlue}, 	内存地址:0xc420076000, 指针的内存地址: 0xc420074000

可以看到在函数passP中,参数p是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc420074000和0xc420074010都指向内存地址0xc420076000。 函数内对*T的改变显然会影响原始的对象,因为它是对同一个对象的操作。

当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T和*T作为参数的时候副本创建的不同。

如何选择T和*T

在定义函数和方法的时候,作为一位资深的Go开发人员,一定会对函数的参数和返回值定义成T和*T深思熟虑,有些情况下可能还会有些苦恼。
那么什么时候才应该把参数定义成类型T,什么情况下定义成类型*T呢。

一般的判断标准是看副本创建的成本和需求。

  1. 不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型T。相反,如果想修改原始的变量,则选择*T
  2. 如果变量是一个 的struct或者数组,则副本的创建相对会影响性能,这个时候考虑使用*T,只创建新的指针,这个区别是巨大的
  3. (不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成T,Go编译器尽量将对象分配到栈上,而*T很可能会分配到对象上,这对垃圾回收会有影响

什么时候发生副本创建

上面举的例子都是作为函数参数时发生的副本的创建,还有很多情况下会发生副本的创建,甚至有些“隐蔽”的情况。
编程的时候如何小心这些情况呢,一条原则就是:

A go assignment is a copy of the value itself
赋值的时候就会创建对象副本

Assignment的语法表达式如下:

Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .

Each left-hand side operand must be addressable, a map index expression, or (for = assignments only) the blank identifier. Operands may be parenthesized.

最常见的case

最常见的赋值的例子是对变量的赋值,包括函数内和函数外:

123456789101112131415161718192021222324252627
package mainimport "fmt"type Bird struct {	Age  int	Name string}type Parrot struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}var parrot2 = parrot1func main() {	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot2, &parrot2)	parrot3 := parrot1	fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot3, &parrot3)	parrot4 := Parrot(parrot1)	fmt.Printf("parrot4:\t\t %+v, \t\t内存地址:%p\n", parrot4, &parrot4)}

输出结果:

1234
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfa0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xfa0c0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc42007e0c0parrot4:		 {Age:1 Name:Blue}, 		内存地址:0xc42007e100

可以看到这几个变量的内存地址都不相同,说明发生了赋值。

map、slice和数组

slice,map和数组在初始化和按索引设置的时候也会创建副本:

1234567891011121314151617181920212223242526272829303132333435363738394041
package mainimport "fmt"type Bird struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}func main() {	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	//slice	s := []Bird{parrot1}	s = append(s, parrot1)	parrot1.Age = 3	fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", s[0], &(s[0]))	fmt.Printf("parrot3:\t\t %+v, \t\t内存地址:%p\n", s[1], &(s[1]))	parrot1.Age = 1	//map	m := make(map[int]Bird)	m[0] = parrot1	parrot1.Age = 4	fmt.Printf("parrot4:\t\t %+v\n", m[0])	parrot1.Age = 5	parrot5 := m[0]	fmt.Printf("parrot5:\t\t %+v, \t\t内存地址:%p\n", parrot5, &parrot5)	parrot1.Age = 1	//array	a := [2]Bird{parrot1}	parrot1.Age = 6	fmt.Printf("parrot6:\t\t %+v, \t\t内存地址:%p\n", a[0], &a[0])	parrot1.Age = 1	a[1] = parrot1	parrot1.Age = 7	fmt.Printf("parrot7:\t\t %+v, \t\t内存地址:%p\n", a[1], &a[1])}

输出结果

1234567
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfa0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200160f0parrot3:		 {Age:1 Name:Blue}, 		内存地址:0xc420016108parrot4:		 {Age:1 Name:Blue}parrot5:		 {Age:1 Name:Blue}, 		内存地址:0xc420012320parrot6:		 {Age:1 Name:Blue}, 		内存地址:0xc420016120parrot7:		 {Age:1 Name:Blue}, 		内存地址:0xc420016138

可以看到 slice/map/数组 的元素全是原始变量的副本,副本

for-range循环

for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
package mainimport "fmt"type Bird struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}func main() {	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	//slice	s := []Bird{parrot1, parrot1, parrot1}	s[0].Age = 1	s[1].Age = 2	s[2].Age = 3	parrot1.Age = 4	for i, p := range s {		fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i + 2), p, &p)	}	parrot1.Age = 1	//map	m := make(map[int]Bird)	parrot1.Age = 1	m[0] = parrot1	parrot1.Age = 2	m[1] = parrot1	parrot1.Age = 3	m[2] = parrot1	parrot1.Age = 4	for k, v := range m {		fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (k + 2), v, &v)	}	parrot1.Age = 4	//array	a := [...]Bird{parrot1, parrot1, parrot1}	a[0].Age = 1	a[1].Age = 2	a[2].Age = 3	parrot1.Age = 4	for i, p := range a {		fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i + 2), p, &p)	}}

输出结果

12345678910
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfb0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200122a0parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc4200122a0parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc4200122a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc420012320parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc420012320parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc420012320parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200123a0parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc4200123a0parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc4200123a0

注意循环变量是重用的,所以你看到它们的地址是相同的。

channel

往channel中send对象的时候也会创建对象的副本:

12345678910111213141516171819202122232425262728
package mainimport "fmt"type Bird struct {	Age  int	Name string}var parrot1 = Bird{Age: 1, Name: "Blue"}func main() {	ch := make(chan Bird, 3)	fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)	ch <- parrot1	parrot1.Age = 2	ch <- parrot1	parrot1.Age = 3	ch <- parrot1	parrot1.Age = 4	p := <-ch	fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", 2, p, &p)	p = <-ch	fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", 3, p, &p)	p = <-ch	fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", 4, p, &p)}

输出结果:

1234
parrot1:		 {Age:1 Name:Blue}, 		内存地址:0xfa0a0parrot2:		 {Age:1 Name:Blue}, 		内存地址:0xc4200122a0parrot3:		 {Age:2 Name:Blue}, 		内存地址:0xc4200122a0parrot4:		 {Age:3 Name:Blue}, 		内存地址:0xc4200122a0

函数参数和返回值

将变量作为参数传递给函数和方法会发生副本的创建。
对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本。

Method Receiver

因为方法(method)最终会产生一个receiver作为第一个参数的函数(参看规范),所以就比较好理解method receiver的副本创建的规则了。
当receiver为T类型时,会发生创建副本,调用副本上的方法。
当receiver为*T类型时,只是会创建对象的指针,不创建对象的副本,方法内对receiver的改动会影响原始值。

不同类型的副本创建

bool,数值和指针

bool和数值类型一般不必考虑指针类型,原因在于这些对象很小,创建副本的开销可以忽略。只有你在想修改同一个变量的值的时候才考虑它们的指针。

指针类型就不用多说了,和数值类型类似。

数组

数组是值类型,赋值的时候会发生原始数组的复制,所以对于大的数组的参数传递和赋值,一定要慎重。

12345678910111213
package mainimport "fmt"func main() {	a1 := [3]int{1, 2, 3}	fmt.Printf("a1:\t\t %+v, \t\t内存地址:%p\n", a1, &a1)	a2 := a1	a1[0] = 4	a1[1] = 5	a1[2] = 6	fmt.Printf("a2:\t\t %+v, \t\t内存地址:%p\n", a2, &a2)}

输出

12
a1:		 [1 2 3], 		内存地址:0xc420012260a2:		 [1 2 3], 		内存地址:0xc4200122c0

对于[...]T和[...]*T的区别,我想你也应该清楚了,[...]*T创建的副本的元素时元数组元素指针的副本。

map、slice 和 channel

网上一般说, 这三种类型都是指向指针类型,指向一个底层的数据结构。
因此呢,在定义类型的时候就不必定义成*T了。

当然你可以这么认为,不过我认为这是不准确的,比如slice,其实你可以看成是SliceHeader对象,只不过它的数据Data是一个指针,所以它的副本的创建对性能的影响可以忽略。

字符串

string类型类似slice,它等价StringHeader。所以很多情况下会用`unsafe.Pointer`与[]byte类型进行更有效的转换,因为直接进行类型转换string([]byte)会发生数据的复制。

字符串比较特殊,它的值不能修改,任何想对字符串的值做修改都会生成新的字符串。

大部分情况下你不需要定义成*string。唯一的例外你需要nil值的时候。我们知道,类型string的空值/缺省值为"",但是如果你需要nil,你就必须定义*string。举个例子,在对象序列化的时候""和nil表示的意义是不一样的,""表示字段存在,只不过字符串是空值,而nil表示字段不存在。

函数

函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。

1234567891011
package mainimport "fmt"func main() {	f1 := func(i int) {}	fmt.Printf("f1:\t\t %+v, \t\t内存地址:%p\n", f1, &f1)	f2 := f1	fmt.Printf("f2:\t\t %+v, \t\t内存地址:%p\n", f2, &f2)}

输出结果:

12
f1:		 0x2200, 		内存地址:0xc420028020f2:		 0x2200, 		内存地址:0xc420028030

参考文档

  1. https://www.reddit.com/r/golang/comments/5lheyg/returning_t_vs_t/?
  2. https://github.com/google/go-github/issues/180
  3. http://openmymind.net/Things-I-Wish-Someone-Had-Told-Me-About-Go/
  4. http://goinbigdata.com/golang-pass-by-pointer-vs-pass-by-value/
  5. https://groups.google.com/forum/#!topic/golang-nuts/__BPVgK8LN0
  6. https://golang.org/ref/spec
  7. https://golang.org/doc/faq
  8. https://golang.org/doc/effective_go.html
  9. https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/
  10. https://dhdersch.github.io/golang/2016/01/23/golang-when-to-use-string-pointers.html
  11. https://dave.cheney.net/2016/03/19/should-methods-be-declared-on-t-or-t
  12. http://colobu.com/2016/10/28/When-are-function-parameters-passed-by-value/
]]>
0
<![CDATA[这才是真正的下半场]]> http://www.udpwork.com/item/16049.html http://www.udpwork.com/item/16049.html#reviews Thu, 05 Jan 2017 11:53:12 +0800 魏武挥 http://www.udpwork.com/item/16049.html

昨天(2017年1月3日)凌晨2点,四番群群友风端在群里扔了一个贴子:弈城Master(P)棋谱大全,然后他说了这样的话:

不管是AlphaGo还是别家的AI,感觉太可怕了。认识到在这个领域人类智力已经被碾压,这是一回事,天天看着最顶尖的棋手扑上去被灭,是另外一回事。

后来,果壳网推送了一篇文章,标题是:突发:神秘AI“Master”已连续击败50名顶尖围棋棋手。

Master继续碾压人类棋手,已经斩落了中日韩等级分第一的柯洁、井山裕太、朴廷恒。至于后来聂卫平上场,其实是蛮搞笑的。聂棋圣年事已高,巅峰之期早已过去,只是辈分和成就放在那里,论棋力,并不能代表人类当下最强大的水平。

 

在几个中国互联网商业大佬提出了“下半场”这个概念时,我是有些哑然失笑的。

做人,还是要多读点书。

这才是真正的下半场

2013年,由东西文库策划的《与机器赛跑》中文版出版。在这本书里,作者埃里克·布林约尔松(Erik Brynjolfsson)与安德鲁·麦卡菲(Andrew McAfee)提到了一个脍炙人口的故事:

国际象棋的发明者得到了国王的一个奖励允诺:第一个放一粒大米,第二个放两粒,第三个放四粒,以此类推,每一格的大米数是前一格的两倍。

提到这个故事的原因,在于这样的一个计算:在棋盘的上半场,大米堆并不是很离谱,在经过32次平方后,国王需要给发明家40亿粒大米,大抵上就是一大片耕地的水平。但到了棋盘的下半场之后,这些大米堆起来会比珠穆朗玛峰还要高。下半场带来的加速增长,将远远超过线性增长,彻底颠覆掉我们的期待。

笔锋一转,两位作者继续写道,

1958年,美国经济分析局将“信息技术”列入了商业投资类别,两位作者认为这可以算成起始年。而根据摩尔定律,每18个月,集成晶管数量翻倍。国际象棋到达第32个格子标志着上半场结束进入下半场。而晶体管数量翻32倍,也就是过去了48年:2006年。

嗯,2006年,被这本书的作者们认为,我们进入了棋盘下半场,一切的发展,将令人瞠目结舌。两位作者举例,

在2004年,一本名为《劳动新分工》的书还认为汽车是无法自动化的,但2010年,谷歌无人驾驶汽车出现。2011年,沃森计算机在《危险边缘》智力问答节目中,战胜了两位最出色的人类参赛者。人类对手在比赛最后一道题的书面回答后补充了一句话:我,欢迎我们的新霸主,电脑。

而到了今天,我们看到,谷歌的阿尔法狗在围棋比赛中,战胜了人类的一流棋手。

这才是“下半场”的真正含义:指数型增长的加速发展,已经到来。

 

人工智能这个词,被提出很久。

1956年夏季,麦卡赛、明斯基、罗切斯特和申农等一批年轻科学家聚会,首次提出了“人工智能”这一术语。

不过,著名的图灵测试,是图灵在1950年一篇论文《计算机器与智能》中提出的。

但人工智能虽然很早就被提出,一直并不是一门显学,通俗地讲,不够火。一度还被认为是一种“不切实际的理论研究”。

直到互联网来临。

一开始互联网对人工智能的研究帮助并不大,但随着互联网的深化,越来越多的人成为互联网使用者之后,海量数据出现。

这对人工智能的推动,有着指数增长般的加速作用。

海量数据,使得机器学习得以成为可能。

人工智能真正的里程碑发生在2012年。当年,吴恩达(Andrew Ng)领导的“谷歌大脑”项目,让机器系统能够以非常低的错误率在海量图像中识别猫。

 

不过,关于阿尔法狗和这个master,到底算不算人工智能,其实是有争议的。

人工智能究竟什么时候能够到来,皮埃罗·斯加鲁菲有点不以为然。

他的说法是:

我并不害怕人工智能,我反而害怕人工智能时代不能尽快到来。

四番群里的另一位群友,他山石智库的创始人李大巍最近和贾斯汀·卡赛尔签了个合作的协议。按照李大巍的说法,这位世界经济论坛的人工智能委员会主席,对AI恐惧,也是付诸一笑。

我对AI恐惧,倒不是很在意,因为我估计我还能活个三十到四十年,这点时间里,说AI就像各种科幻电影或小说里那样描述控制了人类,好像还可以很乐观地认为见不到。

我是一个哪管我死后洪水滔天的人,所以我并不恐惧。

但我的确意识到,机器正在飞速进步。

因为它已经来到了指数型增长的下半场。

 

中国互联网商业大佬讲的下半场,其实是一种很公关的说法:俺们重新开球,重新踢过。意思就是,俺们是同一个起跑线上的。

这种说法的可笑之处在于,如果你不从事互联网高科技,也就罢了。但既然是从业者,难道不了解“指数型增长”这五个字么?

真正的下半场,表明的意思是:它将呈现出一种与上半场截然不同的加速度。

回望我们的上半场,那是二三十年的基础建设。

展望我们的下半场,我们的确无法预知,在这之上的起飞,将会把我们带到哪里。

真正的大未来,并没有来。

因为,你压根不知道。

 

—— 首发 扯氮集 ——

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人。

这才是真正的下半场,首发于扯氮集

]]>

昨天(2017年1月3日)凌晨2点,四番群群友风端在群里扔了一个贴子:弈城Master(P)棋谱大全,然后他说了这样的话:

不管是AlphaGo还是别家的AI,感觉太可怕了。认识到在这个领域人类智力已经被碾压,这是一回事,天天看着最顶尖的棋手扑上去被灭,是另外一回事。

后来,果壳网推送了一篇文章,标题是:突发:神秘AI“Master”已连续击败50名顶尖围棋棋手。

Master继续碾压人类棋手,已经斩落了中日韩等级分第一的柯洁、井山裕太、朴廷恒。至于后来聂卫平上场,其实是蛮搞笑的。聂棋圣年事已高,巅峰之期早已过去,只是辈分和成就放在那里,论棋力,并不能代表人类当下最强大的水平。

 

在几个中国互联网商业大佬提出了“下半场”这个概念时,我是有些哑然失笑的。

做人,还是要多读点书。

这才是真正的下半场

2013年,由东西文库策划的《与机器赛跑》中文版出版。在这本书里,作者埃里克·布林约尔松(Erik Brynjolfsson)与安德鲁·麦卡菲(Andrew McAfee)提到了一个脍炙人口的故事:

国际象棋的发明者得到了国王的一个奖励允诺:第一个放一粒大米,第二个放两粒,第三个放四粒,以此类推,每一格的大米数是前一格的两倍。

提到这个故事的原因,在于这样的一个计算:在棋盘的上半场,大米堆并不是很离谱,在经过32次平方后,国王需要给发明家40亿粒大米,大抵上就是一大片耕地的水平。但到了棋盘的下半场之后,这些大米堆起来会比珠穆朗玛峰还要高。下半场带来的加速增长,将远远超过线性增长,彻底颠覆掉我们的期待。

笔锋一转,两位作者继续写道,

1958年,美国经济分析局将“信息技术”列入了商业投资类别,两位作者认为这可以算成起始年。而根据摩尔定律,每18个月,集成晶管数量翻倍。国际象棋到达第32个格子标志着上半场结束进入下半场。而晶体管数量翻32倍,也就是过去了48年:2006年。

嗯,2006年,被这本书的作者们认为,我们进入了棋盘下半场,一切的发展,将令人瞠目结舌。两位作者举例,

在2004年,一本名为《劳动新分工》的书还认为汽车是无法自动化的,但2010年,谷歌无人驾驶汽车出现。2011年,沃森计算机在《危险边缘》智力问答节目中,战胜了两位最出色的人类参赛者。人类对手在比赛最后一道题的书面回答后补充了一句话:我,欢迎我们的新霸主,电脑。

而到了今天,我们看到,谷歌的阿尔法狗在围棋比赛中,战胜了人类的一流棋手。

这才是“下半场”的真正含义:指数型增长的加速发展,已经到来。

 

人工智能这个词,被提出很久。

1956年夏季,麦卡赛、明斯基、罗切斯特和申农等一批年轻科学家聚会,首次提出了“人工智能”这一术语。

不过,著名的图灵测试,是图灵在1950年一篇论文《计算机器与智能》中提出的。

但人工智能虽然很早就被提出,一直并不是一门显学,通俗地讲,不够火。一度还被认为是一种“不切实际的理论研究”。

直到互联网来临。

一开始互联网对人工智能的研究帮助并不大,但随着互联网的深化,越来越多的人成为互联网使用者之后,海量数据出现。

这对人工智能的推动,有着指数增长般的加速作用。

海量数据,使得机器学习得以成为可能。

人工智能真正的里程碑发生在2012年。当年,吴恩达(Andrew Ng)领导的“谷歌大脑”项目,让机器系统能够以非常低的错误率在海量图像中识别猫。

 

不过,关于阿尔法狗和这个master,到底算不算人工智能,其实是有争议的。

人工智能究竟什么时候能够到来,皮埃罗·斯加鲁菲有点不以为然。

他的说法是:

我并不害怕人工智能,我反而害怕人工智能时代不能尽快到来。

四番群里的另一位群友,他山石智库的创始人李大巍最近和贾斯汀·卡赛尔签了个合作的协议。按照李大巍的说法,这位世界经济论坛的人工智能委员会主席,对AI恐惧,也是付诸一笑。

我对AI恐惧,倒不是很在意,因为我估计我还能活个三十到四十年,这点时间里,说AI就像各种科幻电影或小说里那样描述控制了人类,好像还可以很乐观地认为见不到。

我是一个哪管我死后洪水滔天的人,所以我并不恐惧。

但我的确意识到,机器正在飞速进步。

因为它已经来到了指数型增长的下半场。

 

中国互联网商业大佬讲的下半场,其实是一种很公关的说法:俺们重新开球,重新踢过。意思就是,俺们是同一个起跑线上的。

这种说法的可笑之处在于,如果你不从事互联网高科技,也就罢了。但既然是从业者,难道不了解“指数型增长”这五个字么?

真正的下半场,表明的意思是:它将呈现出一种与上半场截然不同的加速度。

回望我们的上半场,那是二三十年的基础建设。

展望我们的下半场,我们的确无法预知,在这之上的起飞,将会把我们带到哪里。

真正的大未来,并没有来。

因为,你压根不知道。

 

—— 首发 扯氮集 ——

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人。

这才是真正的下半场,首发于扯氮集

]]>
0
<![CDATA[]]> http://www.udpwork.com/item/16048.html http://www.udpwork.com/item/16048.html#reviews Thu, 05 Jan 2017 01:57:24 +0800 elya妞 http://www.udpwork.com/item/16048.html 文/elya 白驹过隙,时光荏苒,2017年悄然而至,不知道你的总结写好了吗?计划订好了吗?骚动的跳槽季,是否有行动计划了?年底的奖金能拿到多少?明年可有晋升机会?好多好多的问题,好多好多的焦虑,很多宝宝陷入了自我认知型焦虑,感觉自己现有的工作做不好,也找不到更好的工作了。最近在知乎live上进行了一场关于求职与跳槽的分享,一共12个法则,希望能帮助到你进行该不该入行、怎样晋升、该不该跳槽的思考。 我对行业一无所知,怎么入行? 我虽然开始做相关工作了,但是非常迷茫,不知道怎么展开? 公司根本不重视我在做的事情,我该怎么办?     法则1:找对人,事半功倍 在我入行的时候,社交网络刚刚兴起,我每天在新浪微博、Twitter、Facebook、Linkedin、若邻网、天际网上像老鹰一样寻觅猎物。我的小猎物,就是各大互联网公司负责招聘的key person,其实找寻方式也很简单,就是搜索目标公司+职位+招聘关键词,一般会发布这种信息的就是Boss本人啦,因为可以直接跟Boss对话,求职效率也高效的多了呢。找到猎物后呢,傻愣愣的搭讪其实很不友好的,我都是先跟踪一段时间,了解清楚Boss的背景和喜好之后,再定向搭讪的,搭讪的原则就是,你得暴露出让对方感兴趣的点,不能说你好我求职就指望人家通过你了,我当时的手段就是频繁给对方产品提建议,果然引起了注意,成功率极高,如果对方让我发简历到邮箱的话,我会在邮件标题注明微博ID、姓名、求职职位、手机号,提升对方的关注效率,简历直接附在邮件正文里,也会在附件插入一份,还会在附件附带一份对方产品的分析报告。数个offer都是这么拿下的。因为这招太好用了,我干脆做了一个基于微博求职的工具叫『微伯乐』,后来全职工作太忙了,这个兼职项目就放下了。现在载体从社交网络变成微信了,但是傻白甜的小朋友还是多,加人就说你好加我,拜托宝宝,你是who?为什么我要加你?你这是买彩票求中奖的加人方法哇!   法则2:圈子生存法 一个人单打独斗机会是有限的,要找到一个匹配你的圈子共同成长,大家互相推荐交换机会是非常不错的职业通道,我在的一个叫做『移动饭醉团』的群组,只有32人,最开始都是pm、编辑、设计,几年后还是32人,群里却出了19个ceo和3个投资人,优秀的人会互相激励。我从MobileUE的群组里也挖了2个人到百度,都是非常优秀的人。如果你还没有自己的圈子,建议赶紧检讨一下,多参与点行业聚会吧!   大家项目文档都做的差不多,我该怎么脱颖而出? 虽然我想定流程标准,但是其他角色也想定,我怎么争取主动权? 工作有段时间了,到底该怎么晋升? 感觉职业发展遇到瓶颈了,该怎么突破?   法则3:自我职业标签 我们有个同事,设计公司出身,特别擅长包装和创新,她的个人职业标签就是创新,在职业晋升的时候,她的创新一直是加分项。我自己的职业标签则是专业方法论体系,不断总结沉淀并分享。你也需要找到一个自己独特的地方,并把这个点无限放大,这是你的职业竞争点,综合优秀的人不常见,单点优秀的人非常多,找到你的点吧!   法则4:了解职称要求 不同的职位有不同的晋升通道,也有不同的职称要求(也有叫任职资格的),职业晋升就是打怪升级,一定要在打怪升级之前,阅读好攻略,你会事半功倍。很多人都是在晋升答辩的时候,才突然发现有某项要求,比如创新,而自己没有做任何这方面的事情,那这一项肯定就没分了呗。我有一次跳级晋升,就是匹配了对应级别的职称要求。   法则5:帕累托法则,20/80法则 帕累托法则就是20/80法则,也叫二八原则,说的是80%的结果是由20%的事情决定的,所以我们要做的是,搞清楚什么是那个20%,如果你在所有项目上均匀分配你的精力,你很难把所有项目都做好不说,也很难出成果,所以找到20%,可能是Boss特别关注,可能是对项目有特殊贡献,把这20%吃的透透的,一定会有好结果。   法则6:项目结果沉淀,项目命名 为啥都是做项目,有人引人注意,而你默默无闻,很多时候不是项目的结果有本质差异,而是他把结果进行了沉淀放大,你则缺少总结和沉淀。百度是有CaseStudy文化的,项目中遇到的问题和取得的成绩都会在CaseStudy会议上被探讨,当然别忘了还要发一封抄送各种大Boss的邮件哦!非常重要的项目,一定要起一个内部代号,雷霆行动啊,闪电计划啊什么的,保证可以被人津津乐道好几年,什么?你叫v2.0项目?肯定转头就忘了好吗?   法则7:方法论沉淀,影响他人 那些被人记住的人,都是因为非常擅长沉淀方法论,比如说俞军,一提到他你就能想到『产品的十二条军规』对吧,最近江湖中的传说俞军又再度出山,提出了新3条,境界再上一层。马化腾的用户体验10/100/1000法则和全民CE,都是影响深夜的方法论。这些听起来太高不可及了,那么我们是否可以从小的做起,比如你做了一个搜索流程,总结了一些搜索心得和模式,是否可以适用于公司其他项目的搜索流程呢?我们就从百度网盘的登录注册,推了一个帐号体系模式设计,复用在公司大部分移动产品的帐号流程。   法则8:跟对人,做对事 最终决定你晋升与否的关键因素,是你的直属领导,记得之前白鸦说过,在一家公司要做到『信、学、干、专』,这个信非常重要,很多人都缺乏对直属领导的信任,背后讨论领导坏话是小圈子的最大话题,其实真可笑,如果你真的质疑人家的能力,离开就好了,如果你死皮赖脸不离开,就要学会信任,并且去学习他的优点,我的百度之旅非常幸运,遇到一个还不错的领导,教了我我很多职场法则,今天我分享的很多,都是从她那儿获得的启发。   我什么时候该跳槽? 想跳槽又不想投简历,怎么办? 未来3~5年我该怎么发展? 最近有若干个朋友跟我聊到中年危机,对未来非常迷茫,其实更多的是对跳槽的恐惧,担心高不成低不就罢了。   法则9:跳槽的净值= (跳的提升-跳的挑战) – (不跳的提升-不跳的挑战) 怎样判断自己该不该跳槽呢?原则就是,先看看跳槽会不会给自己带来提升,你现在是产品经理,跳槽还是产品经理,跳个毛啊?老实呆着吧!如果跳槽变成高级产品经理,并且带一条业务线,那就是加分项了,但是带一条产品线挑战非常之大,也许超越自己的能力,略微减分。这时候你的经理跟你说要给你晋升一级,好了,不跳槽也是高级产品经理,是不是可以再等等,经理又说了晋升的条件是业务数据翻一倍,什么?这个挑战未免有点大!好了,你自己计算吧!   法则10:欲望要纯粹 我之前离开百度的时候,什么都想要,最后就懵掉了不知道怎么选,好在一个朋友提醒我,欲望要纯粹,你最想要的是什么?那是你唯一的抉择标准,好的,我选择了成就感,于是待遇回报就变成其次了,我选了最难的一条路,虽然这条路充满荆棘,但是确实是存在着不断自我突破的成就感的。   法则11:猎头也是我的朋友 很多人都有被猎头骚扰的经历,非常烦,跟房地产销售一样,直接挂电话是最痛快的选择。但是其实不然,中国只是猎头市场鱼龙混杂而已,也是存在好的猎头的,比如说职人社的黄海均老师,就是个人脉和机会的大本营。不妨尝试找到他们与他们做长期的朋友,他会持续告诉你行业的职业动向,平均薪资,哪个公司又释放了机会,至少你是睁着眼睛在行业里看机会的,也不用一换工作就只能投简历啦。   法则12:Key Person四象限 找到你的Key Person,在纸上画四个格子,一个格子是你的导师,是可以在你的职业生涯帮到你的人,不多,3个左右就好,很多人一离职就跟前Boss彻底say goodbye了,其实前Boss都是你的最佳导师人选,很多创业者都是从前领导那里拿到第一笔启动资金呢。第二个格子是你的朋友,朋友嘛无所谓职位高低、贫富贵贱,关键是你需要倾诉的时候能找到他抹一把眼泪鼻涕,听起来好像无关痛痒,但是我有很多创业的朋友完全无法表达倾诉,经常自己晚上裹着毯子发呆,就是没有朋友宫。第三个格子写上你的伙伴,这个格子是可以跟你合伙共事的人,如果你是个设计师,你的朋友都是设计师,你不喜欢pm也不喜欢rd,我只能说你太狭隘了,一定要找到可以跟你一起搭伙的人,这样组合起来才能在你有需要的时候为你所用。最后一个格子写上你的学生,很多人是忽略这一点的,只想跟牛人交朋友,不削于与小朋友为伍,其实不然,小朋友成长三年后未必比你差,所以一定要有慧眼,发现他并且伴他成长,我认识的有成就的人都是谦逊柔和关怀后辈的,尝试做做伯乐吧,回报会让你惊喜的。   最后,推荐大家一本书,《商业模式新生代(个人篇)》,看完后制定一下你的个人商业模式计划书,找到你的职业发展方向,千万不要今天做做ui,明天转了运营,后天转了交互,再后来又转了pm,别人都已经成专家了,你还在小白领域轮岗呢,加油,找到你的方向吧! ]]>

]]>
文/elya 白驹过隙,时光荏苒,2017年悄然而至,不知道你的总结写好了吗?计划订好了吗?骚动的跳槽季,是否有行动计划了?年底的奖金能拿到多少?明年可有晋升机会?好多好多的问题,好多好多的焦虑,很多宝宝陷入了自我认知型焦虑,感觉自己现有的工作做不好,也找不到更好的工作了。最近在知乎live上进行了一场关于求职与跳槽的分享,一共12个法则,希望能帮助到你进行该不该入行、怎样晋升、该不该跳槽的思考。 我对行业一无所知,怎么入行? 我虽然开始做相关工作了,但是非常迷茫,不知道怎么展开? 公司根本不重视我在做的事情,我该怎么办?     法则1:找对人,事半功倍 在我入行的时候,社交网络刚刚兴起,我每天在新浪微博、Twitter、Facebook、Linkedin、若邻网、天际网上像老鹰一样寻觅猎物。我的小猎物,就是各大互联网公司负责招聘的key person,其实找寻方式也很简单,就是搜索目标公司+职位+招聘关键词,一般会发布这种信息的就是Boss本人啦,因为可以直接跟Boss对话,求职效率也高效的多了呢。找到猎物后呢,傻愣愣的搭讪其实很不友好的,我都是先跟踪一段时间,了解清楚Boss的背景和喜好之后,再定向搭讪的,搭讪的原则就是,你得暴露出让对方感兴趣的点,不能说你好我求职就指望人家通过你了,我当时的手段就是频繁给对方产品提建议,果然引起了注意,成功率极高,如果对方让我发简历到邮箱的话,我会在邮件标题注明微博ID、姓名、求职职位、手机号,提升对方的关注效率,简历直接附在邮件正文里,也会在附件插入一份,还会在附件附带一份对方产品的分析报告。数个offer都是这么拿下的。因为这招太好用了,我干脆做了一个基于微博求职的工具叫『微伯乐』,后来全职工作太忙了,这个兼职项目就放下了。现在载体从社交网络变成微信了,但是傻白甜的小朋友还是多,加人就说你好加我,拜托宝宝,你是who?为什么我要加你?你这是买彩票求中奖的加人方法哇!   法则2:圈子生存法 一个人单打独斗机会是有限的,要找到一个匹配你的圈子共同成长,大家互相推荐交换机会是非常不错的职业通道,我在的一个叫做『移动饭醉团』的群组,只有32人,最开始都是pm、编辑、设计,几年后还是32人,群里却出了19个ceo和3个投资人,优秀的人会互相激励。我从MobileUE的群组里也挖了2个人到百度,都是非常优秀的人。如果你还没有自己的圈子,建议赶紧检讨一下,多参与点行业聚会吧!   大家项目文档都做的差不多,我该怎么脱颖而出? 虽然我想定流程标准,但是其他角色也想定,我怎么争取主动权? 工作有段时间了,到底该怎么晋升? 感觉职业发展遇到瓶颈了,该怎么突破?   法则3:自我职业标签 我们有个同事,设计公司出身,特别擅长包装和创新,她的个人职业标签就是创新,在职业晋升的时候,她的创新一直是加分项。我自己的职业标签则是专业方法论体系,不断总结沉淀并分享。你也需要找到一个自己独特的地方,并把这个点无限放大,这是你的职业竞争点,综合优秀的人不常见,单点优秀的人非常多,找到你的点吧!   法则4:了解职称要求 不同的职位有不同的晋升通道,也有不同的职称要求(也有叫任职资格的),职业晋升就是打怪升级,一定要在打怪升级之前,阅读好攻略,你会事半功倍。很多人都是在晋升答辩的时候,才突然发现有某项要求,比如创新,而自己没有做任何这方面的事情,那这一项肯定就没分了呗。我有一次跳级晋升,就是匹配了对应级别的职称要求。   法则5:帕累托法则,20/80法则 帕累托法则就是20/80法则,也叫二八原则,说的是80%的结果是由20%的事情决定的,所以我们要做的是,搞清楚什么是那个20%,如果你在所有项目上均匀分配你的精力,你很难把所有项目都做好不说,也很难出成果,所以找到20%,可能是Boss特别关注,可能是对项目有特殊贡献,把这20%吃的透透的,一定会有好结果。   法则6:项目结果沉淀,项目命名 为啥都是做项目,有人引人注意,而你默默无闻,很多时候不是项目的结果有本质差异,而是他把结果进行了沉淀放大,你则缺少总结和沉淀。百度是有CaseStudy文化的,项目中遇到的问题和取得的成绩都会在CaseStudy会议上被探讨,当然别忘了还要发一封抄送各种大Boss的邮件哦!非常重要的项目,一定要起一个内部代号,雷霆行动啊,闪电计划啊什么的,保证可以被人津津乐道好几年,什么?你叫v2.0项目?肯定转头就忘了好吗?   法则7:方法论沉淀,影响他人 那些被人记住的人,都是因为非常擅长沉淀方法论,比如说俞军,一提到他你就能想到『产品的十二条军规』对吧,最近江湖中的传说俞军又再度出山,提出了新3条,境界再上一层。马化腾的用户体验10/100/1000法则和全民CE,都是影响深夜的方法论。这些听起来太高不可及了,那么我们是否可以从小的做起,比如你做了一个搜索流程,总结了一些搜索心得和模式,是否可以适用于公司其他项目的搜索流程呢?我们就从百度网盘的登录注册,推了一个帐号体系模式设计,复用在公司大部分移动产品的帐号流程。   法则8:跟对人,做对事 最终决定你晋升与否的关键因素,是你的直属领导,记得之前白鸦说过,在一家公司要做到『信、学、干、专』,这个信非常重要,很多人都缺乏对直属领导的信任,背后讨论领导坏话是小圈子的最大话题,其实真可笑,如果你真的质疑人家的能力,离开就好了,如果你死皮赖脸不离开,就要学会信任,并且去学习他的优点,我的百度之旅非常幸运,遇到一个还不错的领导,教了我我很多职场法则,今天我分享的很多,都是从她那儿获得的启发。   我什么时候该跳槽? 想跳槽又不想投简历,怎么办? 未来3~5年我该怎么发展? 最近有若干个朋友跟我聊到中年危机,对未来非常迷茫,其实更多的是对跳槽的恐惧,担心高不成低不就罢了。   法则9:跳槽的净值= (跳的提升-跳的挑战) – (不跳的提升-不跳的挑战) 怎样判断自己该不该跳槽呢?原则就是,先看看跳槽会不会给自己带来提升,你现在是产品经理,跳槽还是产品经理,跳个毛啊?老实呆着吧!如果跳槽变成高级产品经理,并且带一条业务线,那就是加分项了,但是带一条产品线挑战非常之大,也许超越自己的能力,略微减分。这时候你的经理跟你说要给你晋升一级,好了,不跳槽也是高级产品经理,是不是可以再等等,经理又说了晋升的条件是业务数据翻一倍,什么?这个挑战未免有点大!好了,你自己计算吧!   法则10:欲望要纯粹 我之前离开百度的时候,什么都想要,最后就懵掉了不知道怎么选,好在一个朋友提醒我,欲望要纯粹,你最想要的是什么?那是你唯一的抉择标准,好的,我选择了成就感,于是待遇回报就变成其次了,我选了最难的一条路,虽然这条路充满荆棘,但是确实是存在着不断自我突破的成就感的。   法则11:猎头也是我的朋友 很多人都有被猎头骚扰的经历,非常烦,跟房地产销售一样,直接挂电话是最痛快的选择。但是其实不然,中国只是猎头市场鱼龙混杂而已,也是存在好的猎头的,比如说职人社的黄海均老师,就是个人脉和机会的大本营。不妨尝试找到他们与他们做长期的朋友,他会持续告诉你行业的职业动向,平均薪资,哪个公司又释放了机会,至少你是睁着眼睛在行业里看机会的,也不用一换工作就只能投简历啦。   法则12:Key Person四象限 找到你的Key Person,在纸上画四个格子,一个格子是你的导师,是可以在你的职业生涯帮到你的人,不多,3个左右就好,很多人一离职就跟前Boss彻底say goodbye了,其实前Boss都是你的最佳导师人选,很多创业者都是从前领导那里拿到第一笔启动资金呢。第二个格子是你的朋友,朋友嘛无所谓职位高低、贫富贵贱,关键是你需要倾诉的时候能找到他抹一把眼泪鼻涕,听起来好像无关痛痒,但是我有很多创业的朋友完全无法表达倾诉,经常自己晚上裹着毯子发呆,就是没有朋友宫。第三个格子写上你的伙伴,这个格子是可以跟你合伙共事的人,如果你是个设计师,你的朋友都是设计师,你不喜欢pm也不喜欢rd,我只能说你太狭隘了,一定要找到可以跟你一起搭伙的人,这样组合起来才能在你有需要的时候为你所用。最后一个格子写上你的学生,很多人是忽略这一点的,只想跟牛人交朋友,不削于与小朋友为伍,其实不然,小朋友成长三年后未必比你差,所以一定要有慧眼,发现他并且伴他成长,我认识的有成就的人都是谦逊柔和关怀后辈的,尝试做做伯乐吧,回报会让你惊喜的。   最后,推荐大家一本书,《商业模式新生代(个人篇)》,看完后制定一下你的个人商业模式计划书,找到你的职业发展方向,千万不要今天做做ui,明天转了运营,后天转了交互,再后来又转了pm,别人都已经成专家了,你还在小白领域轮岗呢,加油,找到你的方向吧! ]]>

]]>
0
<![CDATA[关于TCP可靠性的一点思考,借此浅谈应用层协议设计]]> http://www.udpwork.com/item/16047.html http://www.udpwork.com/item/16047.html#reviews Wed, 04 Jan 2017 23:27:00 +0800 sunchangming http://www.udpwork.com/item/16047.html 本文主要讨论如何设计一个可靠的RPC协议。TCP是可靠的传输协议,不会丢包,不会乱序,这是课本上讲述了无数遍的道理。基于TCP的传输理论上来说都是可靠的,但是实际这也得看场景。当我做网络游戏的时候也是一直把它当一个可靠的传输协议来用,从没考虑过TCP丢包的问题。直到当我面临像网络存储、机器学习这样领域时,我发现TCP变得“不可靠”了。

具体来说:

  1. 发送方能不能知道已发送的数据对方是不是都收到了?或者,收到了多少?答:不能
  2. 如果怀疑对方没收到,有没有办法可以确认对方没有收到? 答:不能
  3. 我想发送的是“123”,对方收到的会不会是“1223”? 答:是的,会这样,而且无法避免。
第一个问题看起来很傻,众所周知TCP有ACK啊,ACK就是用来对方通知接收到了多少个字节的。可是,实际情况是,ACK是操作系统的事儿,它收到ACK后并不会通知用户态的程序。发送的流程是这样的:
  1. 应用程序把待发送的数据交给操作系统
  2. 操作系统把数据接收到自己的buffer里,接收完成后通知应用程序发送完成
  3. 操作系统进行实际的发送操作
  4. 操作系统收到对方的ACK
问题来了,假如在执行完第二步之后,网络出现了暂时性故障,TCP连接断了,你该怎么办?如果是网络游戏,这很简单,把用户踢下线,让他重新登录去,活该他网不好。但是如果比较严肃的场合,你当然希望能支持TCP重连。那么问题就来了,应用程序并不知道哪些数据发丢了。

以Windows I/O completion ports举个例子。一般的网络库实现是这样的:在调用WSASend之前,malloc一个WSABuffer,把待发送数据填进去。等到收到操作系统的发送成功的通知后,把buffer释放掉(或者转给下一个Send用)。在这样的设计下,就意味着一旦遇上网络故障,丢失的数据就再也找不回来了。你可以reconnect,但是你没办法resend,因为buffer已经被释放掉了。所以这种管理buffer的方式是一个很失败的设计,释放buffer应当是在收到response之后。

Solution:不要依赖于操作系统的发送成功通知,也不要依赖于TCP的ACK,如果你希望保证对方能收到,那就在应用层设计一个答复消息。再或者说,one-way RPC都是不可靠的,无论传输层是TCP还是UDP,都有可能会丢。

第二个问题,是设计应用层协议的人很需要考虑的,简单来说,“成功一定是成功但失败不一定是失败”。我想举个例子。假如你现在正在通过网银给房东转账交房租,然后网银客户端说:“网络超时,转账操作可能失败”。你敢重新再转一次吗?我打赌你不敢。

再举个例子,假设你设计了一个分布式文件存储服务。这个服务只有一条“Append”协议:
  1. 客户端向服务器发送文件名和二进制data。
  2. 服务器把文件打开(不存在则创建),写入数据,然后返回“OK”。中途遇到任何错误则返回“FAIL”
假设你现在有一个20TB的文件,你按照1 GB、1 GB的方式往上传。每发送1 GB,收到OK后,继续发送下1 GB。然后不幸的是中途遇到一个FAIL,你该怎么办?能断点续传吗?NO。因为服务器有可能在写入成功的情况下也返回FAIL(或者网络超时,没有任何回复)。所以你不能重发送未完成的请求。如果你选择从头传,而文件又特别大,那么你可能永远都不会成功。

Solution:采用positioned write。即在客户端发给服务器的请求里加上文件偏移量(offset)。缺点是:若你想要多个客户端同时追加写入同一个文件,那几乎是不可能的。

第三个问题:我想发送的是“123”,对方收到的会不会是“1223”?你想要支持重连、重试,那么你得容忍这种情况发生。

Solution:在应用层给每个message标记一个id,让接收者去重即可。

接下来讨论下如何关闭连接。简单来说:谁是收到最后一条消息的人,谁来主动关闭tcp 连接。另一方在recv返回0字节之后close,千万不要主动的close。

在协议设计上,分两种情况:
  1. 协议是一问一答(类似于HTTP),且发“问”(request)的总是同一方。一方只问,另一方只答
  2. 有显式的EOF消息通知对方shutdown。
如果不满足以上两点的任何一点,那么就没有任何一方能判断它收到的消息是不是最后一条,那协议设计有问题,要改!

(p.s. Windows上还有一种方法,就是用半关连接shutdown(SD_SEND)来标志结束,但是操作起来比较复杂,还不如改协议来的快,容易debug)

]]>
本文主要讨论如何设计一个可靠的RPC协议。TCP是可靠的传输协议,不会丢包,不会乱序,这是课本上讲述了无数遍的道理。基于TCP的传输理论上来说都是可靠的,但是实际这也得看场景。当我做网络游戏的时候也是一直把它当一个可靠的传输协议来用,从没考虑过TCP丢包的问题。直到当我面临像网络存储、机器学习这样领域时,我发现TCP变得“不可靠”了。

具体来说:

  1. 发送方能不能知道已发送的数据对方是不是都收到了?或者,收到了多少?答:不能
  2. 如果怀疑对方没收到,有没有办法可以确认对方没有收到? 答:不能
  3. 我想发送的是“123”,对方收到的会不会是“1223”? 答:是的,会这样,而且无法避免。
第一个问题看起来很傻,众所周知TCP有ACK啊,ACK就是用来对方通知接收到了多少个字节的。可是,实际情况是,ACK是操作系统的事儿,它收到ACK后并不会通知用户态的程序。发送的流程是这样的:
  1. 应用程序把待发送的数据交给操作系统
  2. 操作系统把数据接收到自己的buffer里,接收完成后通知应用程序发送完成
  3. 操作系统进行实际的发送操作
  4. 操作系统收到对方的ACK
问题来了,假如在执行完第二步之后,网络出现了暂时性故障,TCP连接断了,你该怎么办?如果是网络游戏,这很简单,把用户踢下线,让他重新登录去,活该他网不好。但是如果比较严肃的场合,你当然希望能支持TCP重连。那么问题就来了,应用程序并不知道哪些数据发丢了。

以Windows I/O completion ports举个例子。一般的网络库实现是这样的:在调用WSASend之前,malloc一个WSABuffer,把待发送数据填进去。等到收到操作系统的发送成功的通知后,把buffer释放掉(或者转给下一个Send用)。在这样的设计下,就意味着一旦遇上网络故障,丢失的数据就再也找不回来了。你可以reconnect,但是你没办法resend,因为buffer已经被释放掉了。所以这种管理buffer的方式是一个很失败的设计,释放buffer应当是在收到response之后。

Solution:不要依赖于操作系统的发送成功通知,也不要依赖于TCP的ACK,如果你希望保证对方能收到,那就在应用层设计一个答复消息。再或者说,one-way RPC都是不可靠的,无论传输层是TCP还是UDP,都有可能会丢。

第二个问题,是设计应用层协议的人很需要考虑的,简单来说,“成功一定是成功但失败不一定是失败”。我想举个例子。假如你现在正在通过网银给房东转账交房租,然后网银客户端说:“网络超时,转账操作可能失败”。你敢重新再转一次吗?我打赌你不敢。

再举个例子,假设你设计了一个分布式文件存储服务。这个服务只有一条“Append”协议:
  1. 客户端向服务器发送文件名和二进制data。
  2. 服务器把文件打开(不存在则创建),写入数据,然后返回“OK”。中途遇到任何错误则返回“FAIL”
假设你现在有一个20TB的文件,你按照1 GB、1 GB的方式往上传。每发送1 GB,收到OK后,继续发送下1 GB。然后不幸的是中途遇到一个FAIL,你该怎么办?能断点续传吗?NO。因为服务器有可能在写入成功的情况下也返回FAIL(或者网络超时,没有任何回复)。所以你不能重发送未完成的请求。如果你选择从头传,而文件又特别大,那么你可能永远都不会成功。

Solution:采用positioned write。即在客户端发给服务器的请求里加上文件偏移量(offset)。缺点是:若你想要多个客户端同时追加写入同一个文件,那几乎是不可能的。

第三个问题:我想发送的是“123”,对方收到的会不会是“1223”?你想要支持重连、重试,那么你得容忍这种情况发生。

Solution:在应用层给每个message标记一个id,让接收者去重即可。

接下来讨论下如何关闭连接。简单来说:谁是收到最后一条消息的人,谁来主动关闭tcp 连接。另一方在recv返回0字节之后close,千万不要主动的close。

在协议设计上,分两种情况:
  1. 协议是一问一答(类似于HTTP),且发“问”(request)的总是同一方。一方只问,另一方只答
  2. 有显式的EOF消息通知对方shutdown。
如果不满足以上两点的任何一点,那么就没有任何一方能判断它收到的消息是不是最后一条,那协议设计有问题,要改!

(p.s. Windows上还有一种方法,就是用半关连接shutdown(SD_SEND)来标志结束,但是操作起来比较复杂,还不如改协议来的快,容易debug)

]]>
0
<![CDATA[]]> http://www.udpwork.com/item/16046.html http://www.udpwork.com/item/16046.html#reviews Tue, 03 Jan 2017 23:20:50 +0800 alswl http://www.udpwork.com/item/16046.html 今年过年特别早,离春节只剩下二十多天了。 为期 7 天的春节里,工程师们不上班,那万一线上业务出现了故障怎么办? 大公司的朋友们会安排专门的人进行值班(此处心疼一下那些需要大年三十还要值班保证高峰的工程师们), 而作为创业团队人少,难做到在线值守,就需要对线上进行一些整理盘点,找出潜在问题,为春节长假做一些准备。

我们称之为年前大扫除。

大扫除需要做些什么呢,且听我一一道来。

201701/saber.jpeg

PS: 冷知识,大扫除英文是 spring cleaning,所以春节大扫除是 Spring Festival spring cleaning。

大扫除的内容

大扫除其实是一个查漏补缺+囤积粮草的事情。

查漏补缺,即找出潜在的问题。这些问题平时可能不会特意去查看, 借助大扫除这个运动,恰好进行盘点。 计算机的世界里,有一个方法论非常好使,在极多场景可以见到其身影:分层。 TCP 的七层模型,架构设计的 N 层 模型,都是对分层思想的使用。 查漏补缺也不例外,我们可以按照业务访问流程,将需要排查的问题拆分为:业务、应用、中间件、网络、物理、存储 etc。

通过分层,不仅仅完成了自上而下地遍历整个技术栈,也同时将不同模块的内容交给不同的责任方, 确保任务的分割。

分完模块,还要告知大家如何具体查找问题。 这里我介绍一个通用的方法:USE1

For every resource, check Utilization, Saturation, and Errors.

USE 方法是从 Brend Gregg 那里学来的。 在技术设施的领域里,Resource 即是指各种类型的资源,比如 CPU、磁盘、网络、内存, Utilization 指的是使用率,可以简单分为百分制和非百分制。 Saturation 是指饱和率,支持 queue 的资源,就会有这个指标。 Error 即错误,可以从错误统计和日志得知。2

业务领域里面,USE 也有相对应的含义。以审核系统举例, 对应的 USE 可以理解为「审核应用实例跑的 CPU 占用如何,任务队列是否塞满,业务日志是否有异常」。

除了 USE 里面提到的指标,还有几个指标特别重要: TPS 、Latency 和 Capacity。 这几个指标对性能敏感的尤为重要。 检查 USE 的同时,我们必须关注一下这三个指标, 确保 TPS / Latency 是否满足我们预期的 SLA。 哦?压根没有制定 SLA,不要慌,和历史数据对比,先制定一个粗糙的 SLA。 哦?连历史数据都没有?那只能找你 Leader 让他考量一下了。

负责每个子系统的同学,记得检查时候将这些收集到的数据列下来。 在 Metric 做的还不够完善时候,这些数据也是很宝贵的。

在我看来,检查 USE / TPS / Latency ,最大的作用是将抽象的可用性指标描述为几个易于理解的数值进行量化。 一旦能够量化,就可以对比、观测、监控,并且 Review 起来也异常轻松

应对方案

检查出问题之后,就要考虑应对了。时间急任务多,我们的应对方案是是囤积粮草 / 写救命笔记。

囤积粮草比较好理解,基于已有的容量预估,为容易出问题的系统提供一份冗余 。 有些团队平时做基础设施就比较,做 Scale 就是小轻松。 那平时 Scalable 做的不好的朋友,就只能将应用实例多开一些,以避免临时出现的流量波动。

无状态的服务好搞,有状态的 DB 就很难在短时间内做 Scale。 检查这些服务的容量,如果重点资源临近阈值,比如 DB 的硬盘资源,缓存的内存容量。 核心服务的余量在检查中真的发现问题的话,那也只能短期内做扩容了。

对于小团队来说,春节长假的特殊性在于响应会变慢甚至是联系不上。 一旦线上有异常,可能找不到合适的人员来进行处理。 所以第二条写救命笔记则更为重要。 「Google SRE」里面有个小段子,一个绝对不能被按的按钮, 这个按钮会清空内存数据,在飞行过程中被宇航员按了。幸亏美女工程师(下图)写了相关的救命手册, 专门写了针对这种情况的操作,救了这些宇航员的命。

201701/sre.jpeg

图片来自 「Google SRE」

从这个故事里面可以看到,一个紧急操作手册是多么重要。所以在大扫除期间,我们还要补一补平时的文档,将一些常见问题 / 常规操作记录下来。 步骤需要细致到能让让每个远程值班的同学做到 step by step 操作。

啰嗦了这么多,相信大家对大扫除要做些什么已经有所印象了,祝大家过个好年,流量涨涨涨,还能平平安安的。

原文链接:https://blog.alswl.com/2017/01/spring-cleaning/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
]]>
今年过年特别早,离春节只剩下二十多天了。 为期 7 天的春节里,工程师们不上班,那万一线上业务出现了故障怎么办? 大公司的朋友们会安排专门的人进行值班(此处心疼一下那些需要大年三十还要值班保证高峰的工程师们), 而作为创业团队人少,难做到在线值守,就需要对线上进行一些整理盘点,找出潜在问题,为春节长假做一些准备。

我们称之为年前大扫除。

大扫除需要做些什么呢,且听我一一道来。

201701/saber.jpeg

PS: 冷知识,大扫除英文是 spring cleaning,所以春节大扫除是 Spring Festival spring cleaning。

大扫除的内容

大扫除其实是一个查漏补缺+囤积粮草的事情。

查漏补缺,即找出潜在的问题。这些问题平时可能不会特意去查看, 借助大扫除这个运动,恰好进行盘点。 计算机的世界里,有一个方法论非常好使,在极多场景可以见到其身影:分层。 TCP 的七层模型,架构设计的 N 层 模型,都是对分层思想的使用。 查漏补缺也不例外,我们可以按照业务访问流程,将需要排查的问题拆分为:业务、应用、中间件、网络、物理、存储 etc。

通过分层,不仅仅完成了自上而下地遍历整个技术栈,也同时将不同模块的内容交给不同的责任方, 确保任务的分割。

分完模块,还要告知大家如何具体查找问题。 这里我介绍一个通用的方法:USE1

For every resource, check Utilization, Saturation, and Errors.

USE 方法是从 Brend Gregg 那里学来的。 在技术设施的领域里,Resource 即是指各种类型的资源,比如 CPU、磁盘、网络、内存, Utilization 指的是使用率,可以简单分为百分制和非百分制。 Saturation 是指饱和率,支持 queue 的资源,就会有这个指标。 Error 即错误,可以从错误统计和日志得知。2

业务领域里面,USE 也有相对应的含义。以审核系统举例, 对应的 USE 可以理解为「审核应用实例跑的 CPU 占用如何,任务队列是否塞满,业务日志是否有异常」。

除了 USE 里面提到的指标,还有几个指标特别重要: TPS 、Latency 和 Capacity。 这几个指标对性能敏感的尤为重要。 检查 USE 的同时,我们必须关注一下这三个指标, 确保 TPS / Latency 是否满足我们预期的 SLA。 哦?压根没有制定 SLA,不要慌,和历史数据对比,先制定一个粗糙的 SLA。 哦?连历史数据都没有?那只能找你 Leader 让他考量一下了。

负责每个子系统的同学,记得检查时候将这些收集到的数据列下来。 在 Metric 做的还不够完善时候,这些数据也是很宝贵的。

在我看来,检查 USE / TPS / Latency ,最大的作用是将抽象的可用性指标描述为几个易于理解的数值进行量化。 一旦能够量化,就可以对比、观测、监控,并且 Review 起来也异常轻松

应对方案

检查出问题之后,就要考虑应对了。时间急任务多,我们的应对方案是是囤积粮草 / 写救命笔记。

囤积粮草比较好理解,基于已有的容量预估,为容易出问题的系统提供一份冗余 。 有些团队平时做基础设施就比较,做 Scale 就是小轻松。 那平时 Scalable 做的不好的朋友,就只能将应用实例多开一些,以避免临时出现的流量波动。

无状态的服务好搞,有状态的 DB 就很难在短时间内做 Scale。 检查这些服务的容量,如果重点资源临近阈值,比如 DB 的硬盘资源,缓存的内存容量。 核心服务的余量在检查中真的发现问题的话,那也只能短期内做扩容了。

对于小团队来说,春节长假的特殊性在于响应会变慢甚至是联系不上。 一旦线上有异常,可能找不到合适的人员来进行处理。 所以第二条写救命笔记则更为重要。 「Google SRE」里面有个小段子,一个绝对不能被按的按钮, 这个按钮会清空内存数据,在飞行过程中被宇航员按了。幸亏美女工程师(下图)写了相关的救命手册, 专门写了针对这种情况的操作,救了这些宇航员的命。

201701/sre.jpeg

图片来自 「Google SRE」

从这个故事里面可以看到,一个紧急操作手册是多么重要。所以在大扫除期间,我们还要补一补平时的文档,将一些常见问题 / 常规操作记录下来。 步骤需要细致到能让让每个远程值班的同学做到 step by step 操作。

啰嗦了这么多,相信大家对大扫除要做些什么已经有所印象了,祝大家过个好年,流量涨涨涨,还能平平安安的。

原文链接:https://blog.alswl.com/2017/01/spring-cleaning/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
]]>
0
<![CDATA[阴阳师:烧脑的式神 弱智的协作 低劣的反馈]]> http://www.udpwork.com/item/16045.html http://www.udpwork.com/item/16045.html#reviews Tue, 03 Jan 2017 21:46:34 +0800 魏武挥 http://www.udpwork.com/item/16045.html

阴阳师的确很火,上了app store畅销排名第一的货,怎么能说不火。

但这两天有所下滑。

前儿有个朋友在群里贴过一张图,已经掉到第三。我今天去看,回到了第二。img_0677

好奇心日报推送了一篇文章,文中某一段,以运营方在搞召唤弃坑玩家回归的活动策划,来论证这个半年不到的游戏用户流失严重。

这略微有点牵强。

但弃坑玩家的确有。

热情度大幅下降——注意,是大幅,更是多见。

比如我们寮长。

自从肝出一只六星大天狗之后,他每天出没在阴阳师的频率,已经大幅降低。

 

赞美阴阳师的内容很多了,我要说的,是这款游戏很致命的缺陷。

反馈链条非常长。

麦戈尼格尔在《游戏改变世界》一书中,提到游戏的三大元素:有目标、有规则和反馈机制。

img_0678

所有游戏都“有目标”、“有规则”,但反馈机制是一款游戏寿命的核心关键。

阴阳师出挑的地方是画风、音乐、声优,以及各式各样式神卡的复杂度。

但和大多数手游一样,它的反馈机制是很有些问题的。

纯收费的单机游戏,反馈机制就是练级、通关。一旦级练满了,关通了,游戏生命基本结束。

本来就是这样,也不奇怪。

但免费手游不能这么干。

对于免费手游来说,练级、通关都不是它的核心。

免费手游的反馈机制设计,特别烧脑。

 

阴阳师用了类似赌博的方式,来做反馈机制。

也就是或然率来提高玩家的正反馈。

抽到ssr,当然是正反馈——不过,真正玩出精的,都知道ssr其实没啥。

这个游戏的核心物件是御魂,一种可以提升式神能力的装备。

得到一枚优秀的御魂,或然率比抽到ssr卡低很多。而且还需要花费大量的低等级御魂与金币来提升这枚御魂的实力。

运营方很多策划活动,都是奔着提升正反馈去的。

比如说这两天鸟姐那个皮肤。

得到了就有一种正反馈。分享到社交圈,获得其他玩家艳羡,也是一种正反馈。

img_0679

鸟姐皮肤这个策划,极大提高了玩家们这两天游戏的频率,这是不争的事实。

但或然率导致的反馈,是一种双刃剑。

一直没有得到ssr、优秀御魂、鸟姐皮肤的玩家,较为容易产生负反馈,最终弃坑。

所以网上有一篇文章讲抽ssr卡的伪随机,是很有道理的:给予新玩家或长期不能正反馈玩家的干涉性补偿,以延长玩家游戏寿命。

 

但这并不够。

以我们寮寮长为例。

他很早就得到了一枚提升速度的优质御魂,对他的玩家PK大有帮助,所以他是最早上六段的寮成员。

后来他得到了ssr卡大天狗,当天兴奋地把攒着的加技能黑达摩全部喂掉,培养到六星时还录了个小视频到处散发。

时至今日,他的大天狗战力已经7-8000,暴击伤害超过200%,当然还有提高的余地,但他本人已经基本呈半弃坑状态。

img_0680

他连大天狗右下方一个传记都没有激活点开,里面的奖励似乎压根懒得拿。

高等级玩家很容易走到这一步,因为正反馈已经匮乏。

这个游戏练级(这里指的是式神练级,不是主角练级)反馈机制设计得极为漫长。

一个每天玩玩个把小时的休闲玩家大概要花一个月的时间能练成一只五星式神,而一只六星式神需要五只五星式神。

想想这是一个多么痛苦的经历,几乎让你不愿意再来一次。

我一直怀疑一件事:一个正常玩家(不是搞游戏工作室、代练之类的)弃坑比例急剧上升,至少是大幅降低游戏频率和时间的转折点出现在:练出一只六星式神。

 

最近运营方出了一个新道具:蓝色达摩。这个达摩可以迅速提高一个式神的经验。

想必就是冲着式神练级过于痛苦而去。

但杯水车薪,并不解决什么太大的问题。

运营方又不断调整式神的能力,指望玩家们能对以前不屑一顾的冷门或低级式神提起兴趣。

有一点效果,但一想到这些冷门或低级式神真要派上用处,可不又得踏上漫漫练级之路。

想想就令人沮丧。

和很多玩家一样,我个人也希望出运营方出“扫荡”功能,也就是自动打怪自动练级。

但凭良心说一句,并不是好主意。

一来扫荡功能会降低游戏逼格,二来负反馈可能更甚。

我几乎可以推断,如果要出扫荡功能,就肯定会需要扫荡券,扫荡券又是个或然率!

搞不齐还是个需要花钱充值才能得到的货。

 

一个游戏的反馈源,无非就是两个:机器给你反馈,人给你反馈。

对于手游来说,机器能给到的反馈其实很低,或者说,时间很短。

RPG(角色扮演)手游的剧情都很简单,阴阳师这种,一两个月通关不成问题。就算不通关,也无所谓。

当年同样需要狂肝但会让你半夜流泪的仙剑奇侠传,手游上是看不到的。

好的手游一定需要做“人的反馈机制”,因为人作为反馈源,是最为复杂多样的。

在手游里,人作为反馈源,带来的无非就是:仇恨、炫耀、成就。

玩家PK在阴阳师中,称为斗技,能培养出一种“不服明儿再来”的仇恨,或者是“小样打不过吧”的炫耀/成就。

但阴阳师的画风,始终让我觉得,这不是一种以仇恨为主的游戏。斗技场里最高的八段选手,一周得到的主要奖励也不过是能抽三次卡碰碰运气。

我在《狂暴之翼》中,就拼命想上前十(后来大概在前十五左右弃坑),但在阴阳师里,玩了几个月,还在万名之后,完全无所谓。

这种走萌感颜值的游戏,人作为反馈源,合作远远大于竞技。

然而,阴阳师里的人与人合作,实在太差劲了。

 

玩过阴阳师的人都知道,某种意义上,这个游戏就是个单机游戏。

它对合作这件事的不考究,甚至低于我玩的那个狂暴之翼。后者好歹每天还有两次抢箱子需要公会成员通力合作。

阴阳师的组队邀请基本就是个摆设。

本寮是一个小寮,战力普遍较弱,一周前才刚刚打掉一只三星鬼王。

打掉的方式是:两个战力最强的成员当面讨论各自出什么式神,然后开打。而鉴于这个战鬼王的本身设计,他们要当面讨论四次。

为什么必须当面?因为游戏内的聊天打字的话过于艰难,用语音的话,夹杂着游戏音乐声不易听清,转换成文字准确度又不高。而一旦开战,留给你挑选式神的时间是用秒来计算的。

后来这个寮到现在都没再打过三星鬼王,这两家伙碰不到了呗。

阴阳师对于“合作”的近乎弱智的不讲究,使得这个游戏的反馈机制出现了严重的问题。

弃坑的原因无非就是:正反馈不足。

 

阴阳师最出挑的设计就是式神的技能。

可以这么说,9成以上的式神好好培养都有其用武之地。并不是ssr必定碾压r的,甚至最低层级的n卡,赤舌和鬼赤都是很颇有用的主。

这意味着人民币玩家并不见得有绝对优势,这也是这款游戏的核心魅力。

但阴阳师最烂的设计就是玩家协作。

我们寮有两个成员,经常协作打结界和鬼王。

以至于我和我的朋友们说:我怀疑这是一对恋人。

 

我现下并没有弃坑的意思。

但我想,如果寮里的活跃玩家我一个都不认识的话,我大概也会弃坑吧。

六星 八段 战无敌

—— 首发 扯氮集 ——

版权说明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

阴阳师:烧脑的式神 弱智的协作 低劣的反馈,首发于扯氮集

]]>

阴阳师的确很火,上了app store畅销排名第一的货,怎么能说不火。

但这两天有所下滑。

前儿有个朋友在群里贴过一张图,已经掉到第三。我今天去看,回到了第二。img_0677

好奇心日报推送了一篇文章,文中某一段,以运营方在搞召唤弃坑玩家回归的活动策划,来论证这个半年不到的游戏用户流失严重。

这略微有点牵强。

但弃坑玩家的确有。

热情度大幅下降——注意,是大幅,更是多见。

比如我们寮长。

自从肝出一只六星大天狗之后,他每天出没在阴阳师的频率,已经大幅降低。

 

赞美阴阳师的内容很多了,我要说的,是这款游戏很致命的缺陷。

反馈链条非常长。

麦戈尼格尔在《游戏改变世界》一书中,提到游戏的三大元素:有目标、有规则和反馈机制。

img_0678

所有游戏都“有目标”、“有规则”,但反馈机制是一款游戏寿命的核心关键。

阴阳师出挑的地方是画风、音乐、声优,以及各式各样式神卡的复杂度。

但和大多数手游一样,它的反馈机制是很有些问题的。

纯收费的单机游戏,反馈机制就是练级、通关。一旦级练满了,关通了,游戏生命基本结束。

本来就是这样,也不奇怪。

但免费手游不能这么干。

对于免费手游来说,练级、通关都不是它的核心。

免费手游的反馈机制设计,特别烧脑。

 

阴阳师用了类似赌博的方式,来做反馈机制。

也就是或然率来提高玩家的正反馈。

抽到ssr,当然是正反馈——不过,真正玩出精的,都知道ssr其实没啥。

这个游戏的核心物件是御魂,一种可以提升式神能力的装备。

得到一枚优秀的御魂,或然率比抽到ssr卡低很多。而且还需要花费大量的低等级御魂与金币来提升这枚御魂的实力。

运营方很多策划活动,都是奔着提升正反馈去的。

比如说这两天鸟姐那个皮肤。

得到了就有一种正反馈。分享到社交圈,获得其他玩家艳羡,也是一种正反馈。

img_0679

鸟姐皮肤这个策划,极大提高了玩家们这两天游戏的频率,这是不争的事实。

但或然率导致的反馈,是一种双刃剑。

一直没有得到ssr、优秀御魂、鸟姐皮肤的玩家,较为容易产生负反馈,最终弃坑。

所以网上有一篇文章讲抽ssr卡的伪随机,是很有道理的:给予新玩家或长期不能正反馈玩家的干涉性补偿,以延长玩家游戏寿命。

 

但这并不够。

以我们寮寮长为例。

他很早就得到了一枚提升速度的优质御魂,对他的玩家PK大有帮助,所以他是最早上六段的寮成员。

后来他得到了ssr卡大天狗,当天兴奋地把攒着的加技能黑达摩全部喂掉,培养到六星时还录了个小视频到处散发。

时至今日,他的大天狗战力已经7-8000,暴击伤害超过200%,当然还有提高的余地,但他本人已经基本呈半弃坑状态。

img_0680

他连大天狗右下方一个传记都没有激活点开,里面的奖励似乎压根懒得拿。

高等级玩家很容易走到这一步,因为正反馈已经匮乏。

这个游戏练级(这里指的是式神练级,不是主角练级)反馈机制设计得极为漫长。

一个每天玩玩个把小时的休闲玩家大概要花一个月的时间能练成一只五星式神,而一只六星式神需要五只五星式神。

想想这是一个多么痛苦的经历,几乎让你不愿意再来一次。

我一直怀疑一件事:一个正常玩家(不是搞游戏工作室、代练之类的)弃坑比例急剧上升,至少是大幅降低游戏频率和时间的转折点出现在:练出一只六星式神。

 

最近运营方出了一个新道具:蓝色达摩。这个达摩可以迅速提高一个式神的经验。

想必就是冲着式神练级过于痛苦而去。

但杯水车薪,并不解决什么太大的问题。

运营方又不断调整式神的能力,指望玩家们能对以前不屑一顾的冷门或低级式神提起兴趣。

有一点效果,但一想到这些冷门或低级式神真要派上用处,可不又得踏上漫漫练级之路。

想想就令人沮丧。

和很多玩家一样,我个人也希望出运营方出“扫荡”功能,也就是自动打怪自动练级。

但凭良心说一句,并不是好主意。

一来扫荡功能会降低游戏逼格,二来负反馈可能更甚。

我几乎可以推断,如果要出扫荡功能,就肯定会需要扫荡券,扫荡券又是个或然率!

搞不齐还是个需要花钱充值才能得到的货。

 

一个游戏的反馈源,无非就是两个:机器给你反馈,人给你反馈。

对于手游来说,机器能给到的反馈其实很低,或者说,时间很短。

RPG(角色扮演)手游的剧情都很简单,阴阳师这种,一两个月通关不成问题。就算不通关,也无所谓。

当年同样需要狂肝但会让你半夜流泪的仙剑奇侠传,手游上是看不到的。

好的手游一定需要做“人的反馈机制”,因为人作为反馈源,是最为复杂多样的。

在手游里,人作为反馈源,带来的无非就是:仇恨、炫耀、成就。

玩家PK在阴阳师中,称为斗技,能培养出一种“不服明儿再来”的仇恨,或者是“小样打不过吧”的炫耀/成就。

但阴阳师的画风,始终让我觉得,这不是一种以仇恨为主的游戏。斗技场里最高的八段选手,一周得到的主要奖励也不过是能抽三次卡碰碰运气。

我在《狂暴之翼》中,就拼命想上前十(后来大概在前十五左右弃坑),但在阴阳师里,玩了几个月,还在万名之后,完全无所谓。

这种走萌感颜值的游戏,人作为反馈源,合作远远大于竞技。

然而,阴阳师里的人与人合作,实在太差劲了。

 

玩过阴阳师的人都知道,某种意义上,这个游戏就是个单机游戏。

它对合作这件事的不考究,甚至低于我玩的那个狂暴之翼。后者好歹每天还有两次抢箱子需要公会成员通力合作。

阴阳师的组队邀请基本就是个摆设。

本寮是一个小寮,战力普遍较弱,一周前才刚刚打掉一只三星鬼王。

打掉的方式是:两个战力最强的成员当面讨论各自出什么式神,然后开打。而鉴于这个战鬼王的本身设计,他们要当面讨论四次。

为什么必须当面?因为游戏内的聊天打字的话过于艰难,用语音的话,夹杂着游戏音乐声不易听清,转换成文字准确度又不高。而一旦开战,留给你挑选式神的时间是用秒来计算的。

后来这个寮到现在都没再打过三星鬼王,这两家伙碰不到了呗。

阴阳师对于“合作”的近乎弱智的不讲究,使得这个游戏的反馈机制出现了严重的问题。

弃坑的原因无非就是:正反馈不足。

 

阴阳师最出挑的设计就是式神的技能。

可以这么说,9成以上的式神好好培养都有其用武之地。并不是ssr必定碾压r的,甚至最低层级的n卡,赤舌和鬼赤都是很颇有用的主。

这意味着人民币玩家并不见得有绝对优势,这也是这款游戏的核心魅力。

但阴阳师最烂的设计就是玩家协作。

我们寮有两个成员,经常协作打结界和鬼王。

以至于我和我的朋友们说:我怀疑这是一对恋人。

 

我现下并没有弃坑的意思。

但我想,如果寮里的活跃玩家我一个都不认识的话,我大概也会弃坑吧。

六星 八段 战无敌

—— 首发 扯氮集 ——

版权说明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

阴阳师:烧脑的式神 弱智的协作 低劣的反馈,首发于扯氮集

]]>
0