精华内容
下载资源
问答
  • 如何练好嗓子 声音变得浑厚

    千次阅读 2012-07-11 22:42:13
    所谓“阻”,并不是简单的把声音阻挡住,而是不声音直截了当地通过声道奔涌出来,它通过共鸣器加工、锤炼、变得洪亮、圆润、雄浑、优美动听。  要处理好“畅”与“租”的关系,必须进行共鸣训练。一下介绍几...

    下面是一张60分钟声音训练计划表,各位可根据自己的情况有选择、有针对性地组合操练。 


    (1)气泡音:闭口和张口共30秒。 
    (2)轻度哼鸣:20秒。 
    (3)膈肌训练(狗喘气)闭口1分钟,改良的“嘿”“哈”共30秒。 
    (4)慢吸快呼:2次,20秒。 
    (5)慢吸慢呼:2次,20秒。 
    (6)快吸慢呼:4次,2分钟。 
    (7)“丝”——音:20秒、30秒各2次,1分钟。 
    (8)“衣 ”-----音:20秒、30秒各2次,1分钟。 
    (9)搓脸:10秒。 
    (10)转颈:10次,10秒。 
    (11)松下巴:10秒。 
    (12)提颧肌:10次,10秒,手辅和自行交替进行。 
    (13)咀嚼:闭|、张口各20次,30秒。 
    (14)半打哈欠:5次,10秒。 
    (15)撮唇:10次,20秒。 
    (16)合口左右撅唇:10次(左、右为一次),30秒。 
    (17)转唇:8*8拍,30秒。 
    (18)双唇打响:30次,30秒。 
    (19)弹唇:1分钟。 
    (20)b本音:60次,1分钟。 
    (21)ba本音:60次,1分钟。 
    (22)ba-----ba-----ba------ba------,1分钟。 
    (23)顶腮:30次,30秒。 
    (24)刮舌:20次,20秒。 
    (25)伸卷舌:20次,20秒。 
    (26)立舌:10次,30秒。 
    (31)da-----da-----da-----da------;60次,1分钟。 (32)g本音:60次,1分钟。 
    (33)ga本音:60次,1分钟。 


    (34)ga-----ga-----ga-----ga-----:60次,1分钟。 
    (35)ge音:60次,1分钟。 


    (36)ge-----ge-----ge------ge-----:60次,1分钟。 (37)数“数儿”:一口气由1数到30,3次,3分钟。 
    (38)数“数儿”:一口气数10个八拍,3 次,3分钟。 


    (39)数“数儿”:“一二三,三二一,一二三四五六七;七六五五六七,七六五四三二一”,一口气数3到4个回合。2次,2分钟。 
    (40)数“数儿”:“一二三四五六七八,二二三四五六七八---------八二三四五六七八,七二三四五六七八,---------一二三四五六七八”。一口气数下来,2次,2分钟。 


    (41)数枣儿:要求尽可能一口气数20个以上枣儿,2次,2分钟。 
    (42)数“葫芦”:要求一口气数20个以上“葫芦”,2次,2分钟。 (43)喊操:变换节奏进行,2分钟。 


    (44)绕口令:任选10段,10分钟。 
    (45)有针对性的语段或诗词片段练习:10分钟。 


    (27)转舌:8*8拍,1分钟。 
    (28)弹舌:30秒。 
    (29)d本音:60次,1分钟。 
    (30)da音:60次,1分钟。 
    气息是声音的动力来源。充足、稳定的气息是发音的基础。说话时,横在呼出气流通道上的两条声带,迅速地一开一闭,把稳定的气流切成一串串的喷流,进而转换成听得见的峰音,随着舌、唇、腭等器官的运动,不断改变声道的声学性质,将峰音变成能区别的语音,通过胸腔、喉腔、咽腔、鼻腔、口腔组成的共鸣器放大而发出声音。这就是发音的全过程。气息的运用与呼吸、声带、共鸣器等有着直接的关系。为此,着重要进行下列训练: 


    一、呼吸训练。 


    有的人讲话或唱歌声音洪亮、持久、有力,人们赞叹说,他(她)“中气”很足,相反,有的人说话或唱歌音量很小,有气无力,上气不接下气,像蚊子嗡嗡叫一样,使人难以听清,这种人则“中气”不足。其间除了身体素质的区别外,还有一个气息调节技巧问题,即呼吸和讲话的配合、协调是否恰当的问题。 
    1、正常情况下,说话是在呼气时而不是在吸气时间进行的,停顿则是在吸气时进行的。如果是持续时间较长的讲话或朗诵,必然要求有比平时更强的呼吸循环。 
    讲话时的正确呼吸方法,应当采用由胸腹式联合呼吸法(也称丹田呼吸法),即运用小腹收缩,靠丹田的力量控制呼吸。郭兰英在谈到运用这种呼吸方法时说:“唱歌时小肚子常是硬的,唱的越高就越硬” 
    胸腹式联合呼吸介于胸式呼吸和腹式呼吸两者之间,是二者的结合。具体方法如下: 
    (1)、吸气:小腹向内即向丹田收缩,相反,大腹、胸、腰部同时向外扩展,可以感觉到腰带渐紧,前腹和后腰分别向前、后、左、右撑开的力量。用鼻吸气,做到快、静、深。 
    (2)、呼气:小腹差不多始终要收住,不可放开,使胸、腹部在努力控制下,将肺部储气慢慢放出,均匀地外吐。呼气要用嘴,做到匀、缓、稳。在呼气过程中,语音一个接一个的发出后,组成有节奏的有声语言。 
    这种呼吸方法可以使腹部和丹田充满气息,为发音提供充足的“气”,同时,由于小腹向内收缩,胸前向外扩张,以小腹、后腰和后胸为支柱点,为发音提供了充足的“力”。“气”与“力”的融合,为优美的声音奠定了坚实的基础。 
    2、在讲话过程中,要处理好讲话和呼吸的关系,必须注意: 
    第一, 尽可能轻松自如,吸气要迅速,呼气要缓慢、均匀,吸入的气量要适中。 
    第二, 尽可能在讲话中的自然停顿处换气,不要等讲完一个长句才大呼大吸,显得讲话很吃力。还要根据自己的气量来决定是否用中途不便停顿的长句,不要为了渲染和曾强表达效果而勉为其难地为之。那样,会适得其反。 
    第三, 尽可能时讲话时的姿势有利于呼吸。无论是站姿和还是坐姿,都要抬头舒肩展背,胸部要稍向前倾,小腹自然内收,双脚并立平放。这样发音的关键部位??胸、腹、喉、舌等才能处于良好的呼吸准备和行进状态之中。呼吸顺畅,方可语流顺畅。 


    3、 练习呼吸的方法有很多,主要有: 
    (1)、闻花香:仿佛面前有一盆花香花,深深地吸进其香气,控制一会儿后缓缓吐出。 
    (2)、吹蜡烛:模拟吹灭生日蜡烛,深吸一口气后均匀缓慢地吹,尽可能时间长一点,达到25-30秒为合格。 
    (3)、咬住牙,深吸一口气后,从牙缝中发出“咝-----”声,力求平稳均匀持久。 
    (4)、数数:从一数到十,往复循环,一口气能数多少遍就数多少遍,要数的清晰响亮。 
    (5)、用绕口令或近似绕口令的语句练习气息。如: 
    出东门,过大桥,大桥底下一树枣儿,拿着杆子去打枣,青的多,红的少。一个枣儿,两个枣儿,三个枣儿,四个枣儿,五个枣儿,六个枣儿,七个枣儿,八个枣儿,九个枣儿,十个枣儿``````这是一个绕口令,一口气说完才算好。 
    开始做练习的时候,中间可以适当换气,练到气息有了控制能力时,逐渐减少换气次数,最后要争取一口气说完,甚至多说几个枣儿。 


    二、声带训练。 


    在通常情况下,人们说话时,声带的振动频率大约在60-350赫之间。声带的振动频率决定了发音的音响、音高、音色。声带对发音起很大的作用。,声带的好坏,既有先天因素,也靠后天的训练和保护。注意恰当的训练与运用声带,改变声带条件,保护声带,都是提高语音素质的重要方面。 
    1、 声带训练。最基本的方法是,清晨在空气清新处“吊嗓子”:吸足一口气,身体放松,张开或闭合嘴,由自己的最低音向最高音发出“啊”或“咿”的连续声响,。还可以做高低音连续变化起伏的练习。 
    2、 声带运用。声带运用要科学,得当。这主要是指: 
    第一:在长时间计划之前,声带要做准备活动,犹如赛跑前韧带要做准备活动一样。方法是:将声带放松,用均匀的气流轻轻的拂动它,使之发出细小的抖动声,仿佛小孩子撒娇时喉咙里发出后的那种声音。可以逐渐加大到一定分量,使声带启动,以适应即将到来的长时间运动。 
    第二:在人数较多或场合较大的地方讲话时,发音要轻松自然,处理还节奏,停顿,特别是起音要高低适度,控制好音量,充分利用共鸣器的共鸣作用,要运用“中气”的助力来说话,不能直着嗓子叫喊,否则,声带负担过重,会导致声带很快不堪重负,变得嘶哑,影响效果。 
    3、声带保护。为了保护自己的嗓子,要有意识的少抽烟,喝酒,甚至,不抽烟,不喝酒,少吃或不吃有强烈刺激性的食物,那些对声带都有不良影响。不喝过烫或过冷的汤水。 


    三、共鸣训练。 


    声带所产生的音量是很小的,只占人们讲话时音量的5%左右,其他95%左右的音量,需要通过共鸣腔放大得来,。共鸣腔是决定音色的重要发音器官,直接引起语音共鸣的是声带上方的喉、咽、口、鼻四腔,此外,胸腔和头腔也有共鸣作用。说话用声是以口腔共鸣为主,以胸腔共鸣为基础。共鸣器以咽腔为主又可分为高、中、低三区共鸣。高音共鸣区,即头腔,鼻腔共鸣,音流通过该区共鸣,可以获得高亢响亮的声音。中音共鸣区就是咽腔、口腔共鸣,这里是语音的制造场,是人体中最灵活的共鸣区,音流在这里通过,可以获得丰满圆润的声音。低音共鸣区,主要的胸腔共鸣,音流通过该区共鸣,可以获得浑厚低沉的声音。 
    要想使说话的声音好听和持久,就要正确的运用共鸣器。而运用共鸣器的关键在于处理好“畅”与“阻”的对立和统一关系。所谓“畅”,就是整个发音得声道必须畅通无阻,胸部舒展自如,喉部放松滑润,脊背自然伸直,以便声音不憋不挤,形成一个声柱流畅地奔涌出来。所谓“阻”,并不是简单的把声音阻挡住,而是不让声音直截了当地通过声道奔涌出来,让它通过共鸣器加工、锤炼、变得洪亮、圆润、雄浑、优美动听。 
    要处理好“畅”与“租”的关系,必须进行共鸣训练。一下介绍几种简单易行的共训练方法。 
    1, 放松喉头,用“哼哼”音唱歌。 
    2, 学鸭叫声。挺软腭,口腔张开成一圆筒,边发gaga音,边仔细体会,共鸣运用得好的gaga音好听,共鸣运用得不好的gaga音枯燥、刺耳。 
    3, 学牛叫声。类似打电话的“嗯”(什么?)和“嗯”(明白了)。 
    4, 牙关大开合,同时发出“啊”音。 
    5, 模拟汽笛长鸣声。(di)既可平行发音,也可由大到小或由小到大地变化发音。 
    6, 做扩胸运动,同时尽量发高亢或尽量低沉的声音 。 
    7, “气泡音”练习。闭嘴,用轻匀的气流冲击声带,使之发出细小的抖动声。 
    8, 音阶层练习。选一句话,在本人音域范围内,先用低调说,一级一级地升高,然后又一级一级地下降,在一句高一句低,高低交替,一句话又高到低,再由低到高。 
    9, 夸张四声练习。选择韵母因素较多的词语或成语,运用共鸣技巧做夸张四声的训练。如:清——正——廉——洁——,英——勇——顽——强——。 
    10, 大声呼唤练习。假设某人在离自己100米处,大声呼唤:张——师 
    ——傅——,快——回——来——!喂——,那——里——危——险——,快——离——开——!
    展开全文
  • 随着中国人口红利的衰减、互联网流量红利的马太效应显现、用户的时间越来越碎片化、资本投资变得更为理性和务实,未来企业的单位运营成本也会呈现一个线性的,甚至是指数型的增长。这意味着随着成本的大幅度增加,...

    内容简介

    • 脸谱网如何从默默无闻到坐拥二十几亿用户?
    • 爱彼迎、优步何以在短短时间估值超过百亿美元?
    • 领英怎样跨步成为全球领先的职业社交平台?

    这些初创公司实现爆发式成长的共同奥秘就是增长黑客。

    增长黑客是硅谷当下热门的新商业方法论,其精髓在于通过快节奏测试和迭代,以极低甚至零成本获取并留存用户。

    作为最早提出“增长黑客”概念的理论先驱、带领 Dropbox 实现500%增长的实战领军人物,作者在书中分享了如何跨部门搭建增长黑客团队,以及实现用户和利润双增长的具体行动指南。该书一经出版,便被科技公司奉为运营圭臬。

    作者简介

    肖恩 · 埃利斯(Sean Ellis),首屈一指的增长黑客网络社区 GrowthHackers.com 的联合创始人兼 CEO。网站拥有180万全球用户。肖恩于2010年提出了“增长黑客”一词,也是增长黑客大会的发起人。创业公司和财富100强企业的商业智库。《纽约时报》《华尔街日报》《连线》《快公司》、Inc.com 和 TechCrunch 等诸多媒体都对他进行过报道。

    摩根 · 布朗(Morgan Brown),资深创业公司营销专家,与肖恩一同创办了 GrowthHackers.com。曾任 Inman News 首席运营官。目前任职于 Facebook。

    购买纸质版请点这里

    本书内容

    推荐序:以有限的资源获得最大限度的成长

    文/迅雷创始人、远望资本创始合伙人 程浩

    作为十几年的创业老兵,我花了两天时间一口气将这本书读完,然后又花了一天时间画了一个脑图。原因是这本书写得非常翔实生动,把我以前朦朦胧胧的“感觉”重新梳理了一遍,终于 变为很有条理的知识体系。

    必须说,《增长黑客》是每位奋斗在一线的创业者和企业高管们都该认真研读两遍的实战指南。

    对于如何有效获客、留存用户、让业务爆发增长,书中不仅有体系的方法论,更有翔实的实战技巧。任何希望以有限的营销资源,甚至不想额外花钱就能收获最大限度成长的企业家,本书都不容错过。

    特别在国内资本热衷于炒作热点、项目同质化严重的当下,创业竞争环境要比美国严苛许多。国外往往同一赛道只有两三家企业在竞争,而国内同一时间却能涌现出十几个共享单车、共享充电宝的玩家。这时你再说自己的商业模式有多么巧妙、团队背景有多么豪华都没用。创业者更需要的是一套精细化运营的知识体系,帮助他们从乱战中快速脱颖而出。

    这正是《增长黑客》准备告诉我们的创新方法。预算紧张下依靠巧思去挑战庞然无匹的企业巨头成为可能。脸谱网、优步、Airbnb(爱彼迎)和 Pinterest(一个图片分享网站)这些互联网界知名的“独角兽”,正是依靠这一方法论,完成了项目早期的迅猛扩张,进而取得了巨大成功。

    此前,结合迅雷并不平坦的创业经历和十多年企业一线管理的实战经验,我对埃里克 · 莱斯的《精益创业》做了比较深入的研究,也在自身实战基础上做了全新和丰富的解读,甚至在转型投资人后,多次作为创业营的导师,给国内的创业者们讲过精益创业的课程。

    肖恩 · 埃利斯和摩根 · 布朗两位作者呕心撰写的《增长黑客》,可以说正是《精益创业》的姊妹篇。

    因为《精益创业》主要聚焦在如何低成本快速验证需求和解决方案,所以埃里克并没有针对用户增长这块着更多笔墨,毕竟“有需求”是用户增长的前提。但是精益创业显然也不是万能钥匙,特别在国内,随着人口红利和移动互联网流量红利的双双消失,商业环境已完成了从以往粗放式增长到精细化运营的转变,初创企业即使验证了需求和方案,仍然面临如何获客和如何留存的问题。而《增长黑客》主要讲的就是需求验证后,如何获客、如何激活、如何留存,以及如何赚取更多利润的方法。两本书相结合,基本上覆盖了整个创业过程。

    该书详细介绍了增长黑客团队的组成以及运行机制,并拿脸谱网、Yelp(美国最大的点评网站)、优步等大家耳熟能详的公司举了很多例子。增长黑客做过的事情,其实我们过去在互联网创业过程中很多也都亲自实践过,例如怎么做“漏斗模型”、怎么通过分析数据发现问题、怎么优化着陆页面、怎么提高用户付费率、怎么做 A/B 测试……但这些基本上都属于“项目制”。

    简单讲,就是绝大多数国内公司并没有一个专职负责“增长”的团队,作为资深从业者,我也体会到“项目制”有两个不足之处。

    第一,项目制都是临时性质的,都是针对某个具体问题,该问题解决后项目组就解散了,没有人持续关注用户增长,而用户增长又是一个从不间断的事情。

    第二,每个项目的执行水平和项目经理的经验有直接关系,如果没经验再重视也没用,而国内这种人才又普遍稀缺,所以每个优化都落实得参差不齐,也很难形成积累。

    因此我认为,设立一个专职的增长团队很值得借鉴。当然,在具体执行中,我们也要关注增长团队和各业务部门的潜在冲突。

    对于企业最为关注的如何获客,书中特别着重阐述了病毒式营销。病毒式营销的优点显而易见:高爆发性、成本低,甚至免费。对于社交类产品,例如微信、脸谱网,病毒是最自然的推广方式(大家都记得微信刚注册完,就让你邀请通讯录的好友),这点无须赘言。

    但是非社交类产品怎么做病毒式推广就很有技巧了。文中举例 Dropbox(文件储存软件)通过推荐给朋友来获得更大的存储空间,制造病毒式传播,这一点非常巧妙。我也深有体会,当时还把 Dropbox 推荐给几个朋友使用。与之类比,我想到了微信钱包。理论上,支付类产品是明显的工具(工具需要场景驱动,例如你得想好买什么再去支付),和病毒没什么关系,也没有网络效应。

    事实上,腾讯的财付通确实在微信出现之前一直做得不温不火。这样的工具怎么制造病毒式传播?听起来不容易,但我相信大家对后来的过程其实都很熟悉,那就是微信红包!我印象最深的是一个大年三十晚上,迅雷的同事在群里排好队形让我发红包,躲都躲不了。我不得不开通微信钱包。同样,收到我红包的同事也需要开通微信钱包,才能把红包存到银行里。据我所知,仅一个春节就有近亿用户开通了微信钱包,而且腾讯从头到尾没花一分钱,这就是病毒营销的魅力。

    《增长黑客》整本书读下来酣畅淋漓,把我以前比较零散的知识点做了很好的梳理,而且文中举的例子虽然是国外的,但都是主流应用,相对比较熟悉,这对大家吸收也有帮助。此外,这本书翻译得非常干练流畅,用词严谨。在这里亦对译者张溪梦的辛苦工作表示感谢。

    最后想说,对于身处战火之中,争分夺秒、时间宝贵的每位创业者,理解并施行这本书列举的增长黑客方法,会让你们事半功倍。所以预祝每一位读者在阅读后都有自己的收获,这是一本能够让你在商海中取道捷径的“黑客指南”!

    译序:让数据驱动增长

    文/GrowingIO 创始人&CEO 张溪梦

    “If you are not growing,then you are dying!”(如果企业不在增长,那么就是在衰亡!)一句名言,道出了“增长”这一企业运营永恒的真理。无论是上市企业,还是创业公司,或者是投资机构,都非常看中增长的概念,因为它是衡量一个企业最核心的方向标。

    2010年,肖恩 · 埃利斯首次提出“增长黑客”的概念。他认为:增长黑客的唯一使命就是增长。他们所做的每一件事都力求给产品带来持续增长的可能性。在硅谷,像领英、脸谱网、Airbnb、Dropbox 和 Slack(团队沟通交流平台)这样的独角兽企业,早就开始使用增长黑客的方法进行商业实践。它们通过创造性的方法、科学的数据分析工具,可以用极低的费用在短时间内吸引数以百万计的用户,达到数十亿美元的估值。肖恩在十多年前就发现了这个特点,他认为,现代企业必须不断创新,才能实现高效的增长。

    肖恩在美国硅谷被称为“增长黑客之父”,他不但是“增长黑客”理论的奠基人、扶植超过4家企业高速成长的营销和产品顶级专家,也是我在硅谷工作时的一位非常好的朋友。认识肖恩还要从美国高速增长的创业企业 Dropbox 说起。那是在2010年底,我还在领英位于硅谷山景城的总部工作,带领整个数据分析团队为领英全力推动用数据驱动的增长和变现。有一天晚间,我的一位美国同事和我提到 Dropbox 的增长团队希望能够和我交流一下如何用数据来驱动变现和增长。在此之前,很多人曾经和我提到过 Dropbox 这家公司,其用户数量和活跃度在过去几年一直呈现每年接近500%的爆发式成长,是当时硅谷的明日之星之一。每年5倍以上的成长速度在2009年经济危机之后绝对是一种令人惊叹的成就。最令我着迷的部分还是它的早期增长团队的奠基人肖恩 · 埃利斯的增长黑客理论框架,特别是这套思维体系令很多企业无论在商业还是在财务上都跳出了当时急功近利的营销怪圈,而是将关注用户体验和价值作为企业增长的目标。这种核心价值观又在技术和产品的推动下令企业增长效率得到了迅速放大,从而在未来令企业达到商业利益的最大化。

    以往传统的营销方式只切入用户生命周期的开始部分,例如品牌、定位以及获客等早期阶段,因此这类营销方法都比较粗放,市场部门把营销预算打包然后逐步外包给第三方的代理来推行计划和落地,但是今天这种粗放的模式在不断受到各种挑战和质疑。今天的数据化运营已经远远超过传统营销所定义的范畴,而且已经超过了普通企业数字营销的概念。例如我们所知道的网站营销、微信营销、搜索引擎营销、社交媒体营销等等,媒体广告等概念已经渐渐变成每一家公司的必备竞争力。今天的营销概念已经悄悄地扩展到产品内部,以及销售和服务等各个部门和领域。

    特别重要的是,今天的数字化运营已经通过核心产品的研发以及用户体验等工作获得更快速的业务成长。特别在用户的留存上,需要提高各个环节的转化效率,同时在各种渠道上进行优化,迅速用数据找到优化的方式,不断进化和迭代。增长黑客的核心理论就是这种有效而且规则的高级协作和运营的体现。因此,未来的数字化运营,将不再单纯以市场营销为核心,而是贯穿整个客户生命周期,通过各个部门间的协作为客户提供一个整体的、持续的最佳体验。

    2017年以来,营销的核心已经开始从广告、搜索、社交媒体和公众评论的世界向专注于客户体验的方向迅速转化。其结果是,企业的营销战略越来越多地确认了产品团队,而不仅仅是营销部门,对客户体验进行有效管理进而促进增长。“增长黑客”就是这个重要转型时期的理论基础。

    我们的团队从硅谷回到中国开展业务以后,在增长黑客这个领域里看到中美之间仍然存在四个巨大的差异:第一,是否对数据的巨大价值有深刻的认知;第二,是否掌握数据驱动的体系和方法;第三,是否运用数据指导各个业务部门的运营;第四,是否善于利用分析工具代替人力。

    帮助中国企业弥补这些差距并且能够在与外来竞争的对抗中胜出,这是我们创立 GrowingIO 的主要原因。作为一款用户行为数据分析工具,GrowingIO 围绕用户展开营销渠道优化、提升转化、提高留存和变现等方面的分析。这不仅对没有那么多时间和资源的创业公司或项目有用,也可以帮助大公司构建企业级数据驱动体系。今天是大数据技术迅速应用于商业进而产生价值的时代,如何很快地用分析产品来实现商业价值,是一个企业必须具备的核心竞争力。

    2016年底,GrowingIO 举行了国内首次数据驱动增长大会,我们很荣幸地请到肖恩 · 埃利斯专程来到北京为参会者做了几场精彩的增长黑客演讲和培训。在题为 Growth Hacking Success: Setting and Achieving High Impact Growth Objectives(黑客式成功:设立和实现高影响力目标)的演讲中,肖恩 · 埃利斯也是突出强调了数据分析和科学试验对于增长黑客的重要性。

    眼下,中国正在经历一个重要的产业升级过程。随着中国人口红利的衰减、互联网流量红利的马太效应显现、用户的时间越来越碎片化、资本投资变得更为理性和务实,未来企业的单位运营成本也会呈现一个线性的,甚至是指数型的增长。这意味着随着成本的大幅度增加,中国企业必须改变过去粗放型的营销和运营方式,特别是在市场营销、产品制造、销售以及未来的客户服务等各个方面向更加科学、高效的方向转变。肖恩 · 埃利斯的《增长黑客》将为读者带来一个有效的理论和实践框架,希望它能为您的企业带来高速增长!

    前言:低成本、高效率的精准营销(上)

    2008年,我(肖恩)接到了在线存储服务公司 Dropbox 创始人德鲁 · 休斯顿的电话,他告诉我这个刚刚成立一年的公司面临的困境,这马上激起了我的兴趣。当时,Dropbox 的云文件存储与分享服务已经建立起了一个不错的初期粉丝群,尤其受到硅谷技术达人们的青睐。早在 Dropbox 产品完全开发完成之前,休斯顿就在网上发布了一个视频原型,说明该服务将如何运作,这个视频也为他赢得了著名创业孵化器 Y Combinator 的支持,吸引了一大批早期用户。

    视频在聚合类资讯网站 Digg 上发布之后,马上获得疯狂转发,Dropbox 试用版的用户人数也转眼之间从5000人增加到75000人。很显然,休斯顿找到了一个商机。后来,公开版发布之后,又一批用户注册了 Dropbox,也对其提供的服务颇为满意。但是接下来,当休斯顿试图将用户群扩大到技术达人以外的人群时,他却意外碰壁了,而且他面临着巨大的时间压力,因为竞争十分激烈。一家创业公司 Mozy 就比他早三年起步,另一家公司 Carbonite 已经获得了4800万美元的融资,而休斯顿获得的种子资本只有120万美元。与此同时,微软和谷歌这两大超级巨头公司也在进军云储存领域。面对如此强大的竞争对手,Dropbox 该如何扩大其用户群?

    休斯顿在电话中告诉我,他们的初期用户很稳定,但是规模还不够大,他希望我能够帮助他们吸引更多用户。当时,我正准备辞去在创业公司 Xobni 市场副总裁的职位,而这家公司的创始人亚当 · 史密斯正是休斯顿的好友,于是亚当建议我和休斯顿见个面,讨论一下 Dropbox 面临的挑战。那时,在硅谷,我已经在帮助企业获得飞速发展方面积累了一定的名气,特别是帮助像 Dropbox 这样面临激烈竞争却预算有限的公司。我先是成功帮助网络游戏公司 Uproar 获得增长。当索尼、微软和雅虎都在大举进军游戏行业的时候,Uproar 在我的帮助下一跃成为十大游戏网站之一,并且在1999年12月进行 IPO(首次公开募股),当时,Uproar 已经拥有超过520万名游戏玩家。随后,我又和 LogMeIn 合作帮助其增长。LogMeIn 是由 Uproar 创始人成立的一家非常具有创新性的服务公司。当时,其主要竞争对手 GoToMyPC 正在大力进行市场推广。在我的帮助下,LogMeIn 一举成为市场领导者。这其中有何秘诀?我和工程师们合作,利用技术达到一个对他们来说不同寻常的目的——通过设计新方法寻找并获得新客户并从客户身上学习,以优化客户定位、扩大客户群并提高营销投入的效益。

    1994年,我刚刚步入职场时对软件工程还一无所知。当时,我的工作是为一家商业杂志销售广告版面。而那时,企业互联网化的浪潮才刚刚开始,但是我看到了互联网行业的前景。因此当我遇见 Uproar 的创始人时,当即决定拿出我辛苦赚得的一部分销售提成投资到这家公司,并跳槽到这家游戏门户网站,帮助它做销售广告。但是不久之后我就认识到只依赖传统营销手段的局限性,即便披上了新的互联网时代的外衣,诸如网上横幅广告之类的方法,也很难有效地驱动增长。真正让我醒悟的时刻大概是当我试图销售广告版面给当时领先的广告公司盛世长城和奥美而被它们拒绝的时候,它们不肯将 Uproar 网站上的横幅广告位推荐给它们的客户,理由是 Uproar 网站的用户群不够大。当时,我的手头有些紧,担心拿不到自己急需的销售佣金,与此同时,公司创始人也要求我尽快找到办法获取新用户。我采取的第一个办法是在雅虎等门户网站投放付费广告,这有效地促进了增长。但是这种做法成本太高,这和后来德鲁 · 休斯顿在推广 Dropbox 时遇到的难题一样,价格高昂的广告并没有带来足够高的回报。同时,索尼、雅虎和微软也开始推广它们的游戏,在网上铺天盖地地打广告。作为一个年轻的创业公司,Uproar 在资金实力上根本无法与它们抗衡,因此我必须另谋出路。

    这时,我想到了一个办法:创造一种全新类型的广告,让网站主免费在他们的网站上为 Uproar 的游戏做广告。也就是说,网站可以为访问者提供新的有趣的功能,而 Uproar 也获得了在这个网站上展示的机会。创始人同意了这个想法,随后我和工程师团队一起用了短短几周的时间开发了一个新的单人游戏,只需要给网站增加一小段代码就可以将这个游戏添加到任何网站上。这也是最早的嵌入式微件之一。如此一来,网站主成了 Uproar 的广告联盟成员,对于每一个通过他们的网站转到 Uproar 的新游戏玩家,Uproar 只需要支付50美分。这样的低成本对我们来说非常合适,而且因为这个游戏趣味性很强,我们的联盟成员也非常乐意把它添加到他们的网站上。除了通过这些网站获得新的用户,我们也在这个单人游戏上增加了一个“将此游戏添加到你的网站”的选项,使其他网站主可以毫不费力地将游戏放到他们的网站上。

    这个游戏初获成功后,我们测试了几个不同版本的文案、行为召唤和免费游戏,以寻找最有效的组合方式。Uproar 因此获得了爆炸式的增长,这些免费的游戏很快就被投放到了4万家网站上,Uproar 也打败了那些在营销上一掷千金的巨头公司,一跃成为网络游戏的领军者。自那以后,很多其他公司也采用了同样的策略促进增长,其中最著名的例子就是 YouTube。通过嵌入式视频播放器微件,YouTube 的视频得以遍布整个网络,网络视频也因此形成一股风潮。

    正因为我在 Uproar 取得的成功,其创始人随后又邀请我帮助他发展他的下一个创业项目 LogMeIn。LogMeIn 是一个创新性的产品,可以使用户在任何一台联网的电脑上获取他们家中或者办公室电脑里的文件、邮件和软件。在进行了猛烈的搜索引擎营销攻势之后,LogMeIn 起初获得了不少客户注册量,但是很快就进入了平台期。我意识到它面临的是同样的问题:广告成本太高,而收效太少。尤其是为了将产品与强有力的竞争对手 GoToMyPC 区别开来,LogMeIn 在我的建议下从收费模式转为免费增值模式,但如此一来,产品付费收入减少了,营销成本更显高昂,而且每月一万美元的广告支出并没有带来相应的获客效益。尽管我们做了很多不同版本的广告测试,也尝试了各种不同的关键词搜索,试用了不同的广告平台,转化率仍然非常低。对于一个如此实用而且免费的产品,这样的结果令人十分不解。于是我再一次求助于技术以寻找解决问题的新思路。

    我决定试着联系那些注册后却并未使用我们产品的用户,获取他们的反馈意见。我们在注册过程中收集了用户的邮箱地址,于是便给这些用户发邮件,询问他们为何没有使用 LogMeIn。这种做法现在看来再平常不过了,但在当时却是一个颠覆性的想法。仅仅几天过后,我们收到了完全一致的回复:他们不相信我们的服务真的是免费的。当时,免费增值软件模式才刚刚出现,许多人不相信有这等好事。意识到这点之后,我将我的营销团队和技术团队召集在一起进行头脑风暴,让大家献计献策,思考如何改进用户“着陆页”,如何更好地向用户传达这并非圈套,让他们相信 LogMeIn 真的提供免费的产品。我们尝试了许多营销方案和网页设计,但是,这些试验几乎没有带来任何实质性的改变。之后我们决定在页面上增加一个购买付费版本的简单链接,结果这让我们找到了页面设计、信息传达和服务选项的完美组合,使我们的转化率提高了不止两倍。但这仅仅是开始。通过深入分析数据,我们发现,那些下载了软件却并未使用的用户流失情况更加严重。我们不断试验,比如尝试改进安装过程和注册步骤等,最终使转化率大幅提升。这不仅提高了搜索广告的成本效益,同时使我们能够在保证盈利的前提下将搜索广告规模扩大100多倍。这样一来,公司的规模扩大了,也迅速实现了飞跃式的增长。

    再一次,我们只用了几周时间便找到了解决方案。而且方法也很简单,只需要一些创新性的思考,加强整个公司的协作,开展实时市场测试和试验(而且成本极低,甚至为零),并且根据试验结果做出迅速灵活的反应。这些便是后来我创立的增长黑客方法论里的要素,你也将在这本书里读到这个方法论的内容。

    当然,当时并非只有 Uproar 和 LogMeIn 这两家创业公司将编程和营销知识与网络最初的一些特征结合起来以驱动增长。比如,Hotmail 就是最早利用网络产品病毒式传播与自我传播特征的公司之一,它在用户发出的每一封邮件的底部加上了一句非常简单的话,“加入 Hotmail 你也可以发送免费邮件了”,同时附上了一个链接,可以使新用户跳转到着陆页注册新账户。与此同时,网络支付平台贝宝(PayPal)的例子则显示了将产品和知名网络平台相结合所能获得的巨大增长潜力,这个网络平台就是易贝(eBay)。贝宝团队发现易贝上的拍卖人喜欢使用贝宝作为买家的支付方式,于是他们创建了自动链接。自动链接可以自动将贝宝的图标以及注册链接添加到拍卖人所有在拍商品页面,这使易贝上使用贝宝支付的交易数量增加了两倍,因此贝宝在易贝上获得了病毒式的增长。再来看看领英的故事。这家公司在创立的第一年有些不温不火,但在2003年底实现了势如破竹的增长。这是因为它的工程团队找到了一个绝佳的办法,使用户可以毫不费力地上传他们 Outlook 邮箱的通讯录并邀请联系人。这个办法产生了极大的网络效应,加速了领英的增长。在每一个案例中,增长都不是通过传统的广告营销获得的,而是通过编程上的一些巧思,而且都是在预算紧张的情况下实现的。

    如此这般精心设计、扩大并留存用户群的办法并非依靠传统的营销方案、重金打造的发布会或者高昂的广告支出,而是利用软件开发将营销嵌入产品本身。实践证明,这种办法非常有效,而且性价比很高。或许更重要的是,企业收集、储存、分析并实时跟踪大量用户数据的能力在不断提高,即便是小型创业公司也能够以极低的成本、更快的速度和更高的精准度试验产品的新功能、向用户传递信息的新方式或者塑造品牌的新办法以及其他新的营销手段,因此便催生出了一个通过高速度、跨职能的试验来驱动增长的做法,我就此提出了“增长黑客”这一概念。

    在 LogMeIn 的增长策略取得成功之后,我决定专注于用试验的方法帮助初创公司加速增长。所以当德鲁 · 休斯顿找到我,跟我讨论如何与 Dropbox 合作时,我迫不及待地想将我发展出的这一套方法付诸实践。我所做的第一件事就是获得休斯顿的支持,对现有用户做一个简单的调查,以计算 Dropbox 的“不可或缺性得分”(这是我提出的一个评分标准,你将会在本书中读到更详细的说明)。这个调查只问了一个非常简单的问题:“如果你无法再使用 Dropbox,你将有何感受?”用户可以选择“非常失望”“有一点失望”“不失望”或者“不适用——已经弃用产品”这几个选项。(我之所以这样设计这个问题,是因为我发现询问人们是否满意并不能提供有意义的信息,与满意度相比,失望度能更好地衡量用户对产品的忠诚度。)我已经在很多家创业公司做过这项调查,发现只要有40%的受访者表示如果无法再使用一家公司的产品他们会“非常失望”的话,就说明这家公司拥有非常强的增长潜力,而如果这一比例不到40%(表明用户对产品比较冷淡),这家公司就会在业务增长上遭遇很大的困难。虽然我做了多次此类调查,但是当我看到 Dropbox 的结果时,还是吃了一惊,它的分数非常之高,在那些已经充分探索了产品各项功能的用户中,得分更是高得离谱。

    这表明 Dropbox 有着非常大的增长潜力,那么接下来要思考的就是如何释放这一潜力。因此我向休斯顿提议,不再花更多的钱做付费广告,而是通过试验来找到触发增长的其他办法。休斯顿表示赞成,于是邀请我作为临时的市场总裁加入公司,任期6个月。作为麻省理工学院工程系的毕业生,休斯顿已经很好地发挥了他的工程才能,开发出了 Dropbox 这个优秀的产品,现在我们需要将他的能力用于拓展新的用户群,并保证新用户也会喜欢这个产品。

    接下来便进入了增长过程的第二步——深入分析用户数据。通过分析 Dropbox 的用户数据,我们发现,1/3的 Dropbox 用户都是通过既有用户的推荐注册的。这说明尽管 Dropbox 的产品自传播还没有带来足够快的增长,但它的口碑效应已经十分强劲。也就是说,休斯顿已经创造出了一个人们真正喜爱的产品,而且他们十分乐于向朋友们推荐这个产品,只是它还没有带来足够多的新用户。这是一个典型的“梦幻之地”谬论,至今仍然在创业群体中广为存在:商家相信它们所需要的只是打造出一个出色的产品,而客户的获得只是水到渠成的事。

    我开始思考,Dropbox 如何才能利用并扩大其强有力的口碑效应,使早期用户能够更有动力并且毫不费力地向他们的朋友宣传这个产品。我和休斯顿以及他为这个项目找来的实习生阿尔伯特进行了头脑风暴,我们决定像贝宝那样开展一个用户推荐计划。唯一的问题是贝宝提供10美元的存款返现以激励老用户推荐新用户,虽然它没有披露最终的总成本(联合创始人埃隆 · 马斯克后来透露成本高达六七千万美元),但是 Dropbox 无论如何也没有足够的财力通过“购买”用户来达到期望的增长规模。但我们随后想到,如果我们可以提供对用户有价值的其他东西呢?比如提供更大的存储空间以获得用户的推荐。当时,Dropbox 使用的是亚马逊几年前推出的廉价的 S3 网络服务器来存储数据,这意味着要增加更多存储空间是很容易的事,成本也很低。于是我们将贝宝作为样板设计了一个用户推荐计划,用户每推荐一个朋友使用我们的服务,我们就额外提供给他们250兆的存储空间,同时他们的朋友也可以额外获得250兆的空间。那时,提供250兆就相当于免费赠送了一个硬盘,因此作为激励措施,还是相当“给力”的。

    用户推荐计划启动后,效果立竿见影,通过邮件和社交网络发出的邀请量瞬间激增,推荐注册量增加了60%。毫无疑问,计划奏效了,但是我们并没有止步。我们决心充分把握住这次机会,废寝忘食地努力了几个星期来优化这个计划的每一个细节,包括从信息传达到产品呈现、邮件邀请,再到用户体验和界面元素的细节。我们进行了“快节奏试验”(这是我提出的叫法),并实时评估试验的效果。我会每周两次查看每一个新试验的结果,看看哪些做法有效、哪些无效,并且利用这些数据决定下一次试验的内容。通过不断地调整和改变,结果越来越好,到2010年初时,Dropbox 用户每个月都会发出超过280万份邀请,用户数量也从产品发布时的10万增长到了400多万。这是短短14个月的成果,而且在此期间没有任何传统的营销支出,没有横幅广告,没有付费推广,也没有邮件列表营销。事实上,在我于2009年春季结束和 Dropbox 的合作之后的9个月里,Dropbox 没有再招聘一个新的全职营销负责人。

    与此同时,这一促进市场增长和获客的新方法也开始风靡硅谷。它摒弃了传统的高额营销预算和不科学、不可量化的营销策略,代之以更具成本效益比、更有效果且以数据驱动的模式。其他公司的创新者也开始设计类似的方法,同样聚焦于快速形成并测试开创性的增长思路。2007年底,脸谱网正式成立了一个五人增长团队,称为“增长圈子”。这个团队由产品管理、互联网营销、数据分析和工程方面的专家组成,也包括其最资深的产品经理内奥米 · 格雷特。团队由野心勃勃的经理卡马斯 · 帕里哈比提亚领导,他是脸谱网平台与广告产品的前产品营销负责人。他向马克 · 扎克伯格提议,重新将关注点放到扩大用户数量上。虽然当时的脸谱网已经有大约7000万用户,可以说已经获得了显著的增长,但是它似乎开始止步不前了。所以扎克伯格要求团队测试各种方法,全力突破这一瓶颈。团队捷报频传,让扎克伯格看到了成立增长团队带来的回报,因此他继续为团队增加人手,使团队能够做更多的试验,不断加速脸谱网的增长。

    他们比较大的一次突破是开发了一个翻译软件以推动国际化的增长,而这一次突破也充分体现了增长黑客方法与传统营销手段的区别。当时,脸谱网7000万用户中大多数都生活在北美,这说明国际市场蕴藏着脸谱网最大的增长机遇之一。但是要吸引国际用户,就需要将网站翻译成各种不同的语言版本,这任务太艰巨了。如果选择传统办法,就需要选出十种使用人数最多的语言,然后逐一组建当地团队开展翻译工作。但脸谱网的增长团队没有这么做,团队的工程师在哈维尔 · 奥利凡的带领下开发出了一个翻译软件,利用众包模式,让网站自己的用户来将网站内容翻译成自己的母语。曾在脸谱网增长团队工作过的增长黑客专家安迪 · 约翰斯是这样评价这一创新的:“增长并不是说你要选择最重要的20个国家,在每个国家雇10个人,然后等着这一切的努力能够奏效。增长是通过设计出可以大规模使用的系统,让用户来替我们实现产品的增长。”约翰斯认为脸谱网能够发展到今天的庞大规模,翻译引擎的研发功不可没,可以说是脸谱网采取的最重要的方法之一。

    随着脸谱网用户数量的不断扩大,增长黑客方法也逐渐兴起(当然和脸谱网的发展速度不可同日而语)。这在一定程度上是由于脸谱网增长团队的成员去了新的创业公司,比如 Quora(一个互联网应用服务问答网站)、优步、AsaNa(团队任务管理平台)和推特等,并将这些方法也带了过去。那段时间,我在另外两家创业公司 Eventbrite(在线活动策划服务平台)和 Lookout(远程监控软件)通过增长黑客方法取得了巨大成功,而与此同时,领英、Airbnb 和 Yelp 等一众公司也在践行类似的基于试验的方法。

    以 Airbnb 为例,其创始人在吸引用户方面曾遭遇极大的困难,以至于在公司开始起飞之前他们曾三次发布网站信息。同时,他们的资金链一度极为紧张,在2008年美国总统大选期间,他们甚至通过销售麦片补贴公司收入,还聪明地把当时的两位总统候选人奥巴马和麦凯恩放到了品牌名字里和麦片包装盒上。后来他们的资金困境越发严峻,两位创始人布莱恩 · 切斯基和乔 · 杰比亚一度只能靠没有卖出去的麦片过活,直到他们又筹到更多的资金。他们的团队为扩大用户群尝试了各种办法,但都以失败告终,直到他们找到了一个绝妙的增长黑客方法,从而释放了巨大的增长潜力,而这一过程后来也成了硅谷的一个传奇。他们通过复杂的编程和大量的试验调整,找到了一个可以将用户发布在 Airbnb 上的房源信息自动免费发布到分类广告网站 Craigslist(克雷格列表)上的方法。如此一来,每当有人在该网站上搜索度假租房信息的时候,Airbnb 的房源信息就会出现在结果中。

    这实在是个绝妙的办法。因为 Craigslist 并没有提供给 Airbnb 或任何人跨平台自动发布广告信息的有效方式。Airbnb 的团队不得不进行逆向工程,摸清 Craigslist 如何管理新发布的广告信息,然后在他们的程序中重建这一过程。这意味着他们必须破解 Craigslist 发布系统的运作方式,弄清楚不同城市的度假房屋出租信息被划分到哪个类别以及信息发布有哪些限制条件,比如对图像和其他格式的限定。现任职于优步负责用户增长的安德鲁 · 陈评论称:“总而言之,这样的对接并不简单,需要做大量细致的工作,想必他们请了一帮非常聪明的技术达人花了很多时间进行完善。”他说:“说实话,一个传统的市场人员几乎不可能想到这一点,这种做法涉及太多的技术细节,只有工程师,而且是专注于从 Craigslist 获取更多用户的工程师才能想出来。”

    这个无比复杂的对接程序使 Airbnb 的房源信息可以迅速发布到 Craigslist上,于是数百万 Craigslist 用户通过点击链接跳转到了 Airbnb 网站。在没有花一分钱打广告的情况下,Airbnb 的客房预订量一飞冲天。完成了这一对接之后,Airbnb 团队开始探索如何充分利用这片“蓝海”,他们开始分析并优化用户对 Craigslist 广告信息的反应,优化广告信息的排版、标题等等。虽然最后 Craigslist 禁止了 Airbnb 的这一做法,但是这已经给 Airbnb 带来了巨大的增长动力,而其团队也继续试验其他方法进一步推动增长。他们今天仍然在利用增长黑客方法驱动增长,我们在后面几章会再介绍他们近期的一些成功试验。

    增长黑客方法打破了企业内部传统的“筒仓”结构,将数据分析、工程、产品管理和市场营销方面的员工凝聚起来组成跨职能通力协作的团队,使企业能够将强大的数据分析、技术知识和营销能力高效结合起来,迅速寻找更具潜力的增长手段。通过迅速测试新想法、新思路,并根据计划指标对结果进行评估,增长黑客方法能够帮助企业更快地找到有效的做法、抛弃无效的做法。这可以使企业不再一味地在那些没有用的产品功能或者市场营销手段上浪费时间,摒弃浪费资源、过时而且没有得到验证的做法,代之以经过市场验证的、以数据驱动的做法。

    前言:低成本、高效率的精准营销(中)

    谁可以成为增长黑客?

    增长黑客方法并不仅仅是市场人员的工具,它也可以用于新产品开发和老产品的持续改进,或用于扩大既有产品的用户群。因此,它对于产品开发人员、工程师、设计师、销售人员和经理都有同样重要的价值。

    增长黑客方法也不仅仅是创业者的工具,它既适用于小型初创公司,也适用于大型成熟企业。事实上,如果你在大公司工作,并不需要获得某个公司高层的授权来实施增长黑客方法。它既适合大规模推行(整个公司范围内),也适合小规模实践(一个项目或计划)。也就是说,任何部门或项目团队都可以通过本书中介绍的方法进行增长黑客实践。

    正是这一方法论推动了上述所有公司取得巨大成功,也推动了很多其他增长最快的硅谷独角兽公司取得成功,包括 Pinterest、BitTorrent(一款下载工具)、优步、领英等。对于这些公司取得的爆发式增长,一个常见的说法是它们只是想出了一个颠覆性的商业构想,一个绝妙的、变革性的、像风暴般震撼整个市场的想法。但是这样的说法显然是错误的。对于所有这些成功的公司来说,产品大范围普及并不是一夜之间实现的,也绝不是轻轻松松就达成的。这些公司的成功并不是因为某个改变世界的产品,也不是因为某一个想法、某一次好运气或者天才之举。事实上,它们之所以能够成功,是因为它们遵循了这样一套方法,迅速提出并测试产品开发和营销的新想法,并利用用户行为数据寻找驱动增长的制胜方法。

    如果你觉得这一个迭代过程听起来有些熟悉,很可能是因为你在敏捷软件开发(agile software development)或者精益创业(the Lean Startup methodology)方面了解过类似的做法。这两个理念是将这样一套方法分别用于产品开发和新商业模式探索,而增长黑客则是将同样的方法用于用户获取、留存与收入增长。采用这样的方法对肖恩和其他创业团队来说再自然不过了,因为设计出这一套增长黑客方法的公司汇集了很多熟悉这些方法的工程师,而这些公司的创始人也乐于将工程师开发软件、开发产品的这一套方法应用于扩大用户群。敏捷开发的核心是加快开发速度,强调迅速编程,然后定期测试并进行产品迭代。精益创业同样注重快速开发和高频率测试,并且强调尽快推出一个“最简化可实行产品”(minimum viable product)使用户能够尽快使用,以便尽早获得真实的用户反馈,确保公司业务具有可行性。增长黑客方法采用了这两个理念中持续改进和快速迭代的做法,将之用于客户和收入增长。在这一过程中,增长黑客方法打破了营销和工程之间的传统壁垒,寻找嵌入产品本身的新的营销方法,而这样的方法只能通过更多的技术知识来实现。

    这些开创者以及后继者摸索出的增长黑客做法逐渐形成了一套严密的商业方法论,并且在全球范围内掀起了一股风潮,数以十万计的公司纷纷开始效仿。这些增长黑客包括企业家、营销人员、工程师、产品经理、数据分析师等,他们不仅来自技术创业公司,也来自各行各业,包括技术、零售、B2B(企业对企业)、专业服务、娱乐甚至政界。

    虽然每一个公司的具体实践方法可能有所不同,但是这一套方法论的核心内容都是一样的。

    • 设立一个跨职能团队或几个团队,打破营销和产品开发部门之间传统的筒仓,凝聚公司人才。
    • 进行定性研究和定量数据分析,深入了解用户行为与喜好。
    • 迅速产生新思路并进行测试,根据严格的指标对试验结果进行评估并采取相应行动。

    然而,尽管增长黑客方法论已经通过实践检验,也日益普及,而且可以应用于任何领域或行业,但目前仍然没有一本权威、详尽且具有指导性的书来引导各行各业、不同规模的企业实践这一方法论。这本书正是为了弥补这一空白。

    一本确切翔实的指南

    我们决定写这本书,是因为我们看到了增长黑客能够给各行各业、各种职能带来的巨大潜力,也是因为我们意识到人们对于更好地理解并执行这个理念存在迫切的需求。增长黑客是颠覆性的、强有力的开拓市场的全新手段,但是人们还没有很好地理解该如何利用它以发挥它最大的功效。

    肖恩不仅是增长黑客的一个开拓者,也是网GrowthHackers.com(一家提供黑客式增长咨询的网站)的创始人。这是目前在增长黑客领域领先的资讯网站,并且已经成为这一充满活力的群体的聚集地,其会员遍布世界各地,也吸引着数百万访问用户。我们每天都被各种询问增长黑客最佳做法的问题轰炸,这显然说明人们对于增长黑客的原理和实施过程存在着太多的疑问。于是我们决定写一本确切翔实的指南,使营销人员、产品经理、项目开发者、公司创始人以及各类企业的创新者都能够利用这本指南将增长黑客方法带到他们的团队或者公司中。

    在本书中,我们会分析肖恩在 Dropbox、Uproar、LogMeIn 以及其他很多非常成功的公司的经验,也包括他在发展他自己的两家公司 GrowthHackers.com 和 Qualaroo 过程中的心得体会。Qualaroo 是一家用户研究与调查公司,在肖恩的领导下取得了飞速增长,后来肖恩高价卖掉了它。我们将探究当下其他一些发展最迅速的公司的增长团队的经验,比如脸谱网、印象笔记(Evernote)、领英、Yelp、Pinterest、HubSpot(数字营销公司)、Stripe(网上支付平台)、Etsy(网络商店)、BitTorrent 和 Upworthy(资讯网站),也采访了那些将增长黑客理念带入沃尔玛、IBM(国际商业机器公司)和微软等大型成熟企业的增长负责人。这些内容都将在本书中呈现。我们结合自身经验和这些增长专家的智慧及他们的故事写就这本增长黑客指南,使读者可以从中汲取灵感并付诸实践,实现他们自己的商业目标。

    于是,我们就有了第一本充满实践性、通俗易懂、循序渐进的增长黑客指南,它适用于任何类型的团队、部门或者公司。而两位作者中,一位是增长黑客的奠基者之一,另一位则是增长黑客的资深践行者。

    势不可当的增长机器

    毋庸置疑,增长停滞是当今企业面临的最危险、最紧迫的难题之一,不仅对初创企业来说是这样,对于任何规模、任何行业的企业来说都是如此。《哈佛商业评论》上一篇关于增长停滞的文章指出,一项大规模研究发现,87%的受访企业都经历过一次或多次急剧的增长放缓的情况,而且“在增长停滞的前后10年间,公司市值平均会缩水74%”。而且,两位作者认为这一问题在未来将进一步加剧,“种种迹象表明,在不远的将来,增长停滞的风险会不断上升”,这主要是由于“成熟商业模式的‘半衰期’在缩短”。两位作者提出了造成增长停滞的各种原因,其中包括“企业在管理既有产品和服务更新换代及开发新产品和服务方面的内部流程上存在问题”,以及“过早放弃核心业务,也就是未充分挖掘既有核心业务蕴藏的增长机会”。而增长黑客为这两个问题提供了解决方案。

    简而言之,每一个公司为了生存和发展壮大,都需要扩大客户群。但增长黑客并不仅仅涉及如何获取新客户,而且还包括如何吸引、激活用户并使用户产生依赖,如何灵活地适应客户不断变化的需求和喜好,使他们不仅成为我们不断扩大的收入来源,也成为我们忠实的宣传者,通过口碑推动品牌或产品的增长。

    增长团队的一个核心使命就是尽一切可能挖掘一个产品或服务的增长潜力。这就需要他们不断对产品进行调整并对调整内容进行测试,这包括产品的特征、信息传达方式以及用户获取、留存与变现的方式。这一方法的一个重要目的也在于寻找新的产品开发机遇,可能是通过分析客户行为或反馈,也可能通过研究机器学习和人工智能等新技术的应用方式来寻找机会。

    在很多推行增长黑客方法的公司,这一方法都取得了巨大的成功,以至增长团队发展到超过一百人的规模,而这些增长团队往往下设负责不同任务的小组,比如负责客户留存或者发展移动客户群的小组。有的公司甚至设立了不同人数的小组,并根据不同的业务需求调整小组的人员配置和职责分工。例如,在领英,增长团队已经从一开始的15人扩大到了超过120人,分为5个部门,分别负责网络增长、SEO/SEM(搜索引擎优化/搜索引擎营销)操作、用户引导、国际增长、用户吸引与重获。而在优步,增长团队则由不同小组构成,有负责司机用户增长的,有负责乘客用户增长的,有负责国际市场拓展的,等等。

    现在,任何公司都应该设立一个或多个增长团队,这样做的同时也不需要抛弃传统的组织架构或营销策略。增长团队并不一定会取代传统部门,而是对传统部门的补充,并能帮助它们改进工作方式。创业公司在创业初期摒弃传统的筒仓结构是有利的,但是随着公司的增长,在保留增长团队的同时也可以逐渐建立起传统的营销团队。而在规模更大、更成熟的公司,增长团队可以很好地协助产品、市场、工程和商务智能部门,与它们通力合作并促进不同部门之间的有效沟通。

    肖恩在 Dropbox 的经历表明,即便是规模最小的团队也可以实践这一过程。对于很多创业公司,尤其是初创公司来说,这样的小团队应当由创始人领导,并且整个公司的员工都应当参与进来。而规模更大的公司不可避免地要遭遇既有架构和文化对于改变的抵触情绪。小型增长团队可以单独设立,甚至可以为短期项目专门设立,比如新产品发布或者某一营销渠道(比如移动渠道)的推出。增长团队既可以是从零打造的专注增长的组织单元,也可以是由公司不同部门的现有员工组成的群体,也可以是根据具体需求设立的特别小组。随着时间的推移以及公司具体需求的变化,很多增长团队的规模、工作范围和职责也随之不断变化。

    增长黑客方法能够灵活适应任何团队或公司的不同需求,无论它们的规模是大是小,或是处于哪个增长阶段。它也能够带来丰厚的回报。接下来将讨论增长黑客方法的作用,以及它的重要性缘何远超以往。

    顶住冲击

    今天,不论是最青涩的创业公司还是最成熟的大企业,每一家公司都应当实行增长黑客方法,否则,势必会遭受来自采用此方法的竞争对手的冲击。

    即便是像 IBM 和沃尔玛这样的大型老牌企业现在也开始将增长黑客作为生存的关键手段。毕竟,现在所有公司都在某种程度上成了互联网技术公司,尽管很多公司的互联网操作仅限于营销和销售而非产品开发。另外,今天的市场领导者可能转瞬间就会遭受剧烈冲击,因此也需要迅速地采用新的技术工具并持续进行产品开发与市场营销试验,而这样的需求正迅速地从数字产品领域蔓延到各行各业。

    随着物联网的迅速发展,越来越多的产品都在通过联网或与其他产品联结而变得更加“智能”,这一过程只会发生得越来越快。随着实物商品的世界和软件世界迅速融合,对产品进行实时监测与更新在不久的将来不仅将成为可能,也将成为企业保持竞争力的关键。通用电气 CEO(首席执行官)杰夫 · 伊梅尔特最近表示,“每一个工业企业都将成为软件公司”,而对于消费品公司、媒体企业、金融服务公司等各行各业的公司来说也将如此。著名商业战略分析师迈克尔 · 波特和软件公司 PTC 的 CEO 詹姆士 · 海普曼在他们共同发表在《哈佛商业评论》上的一篇文章中指出,一家公司如果能在产品售出之后对产品保持关注,就意味着“这家公司客户关系的重心从销售这个一次性的交易转移到了逐渐将客户从产品中获得的价值最大化上”。他们强调,这样的转变意味着“产品设计、云操作、服务改进和客户参与等部门之间将需要进行协调与合作”。根据我们的经验,要做到这一点,搭建一个跨职能的增长团队是最佳也是成本效益比最高的办法。

    电动汽车的开拓者特斯拉就是一个例子。它很好地利用技术不断地测试、更新并改进其产品,在这一过程中也将新的竞争对手甩在了身后。特斯拉并不在其生产的汽车上体现车型和年份,因为它不会让客户等着新车型发布,而是定期向车内软件发送更新,实时提升汽车的各方面性能(比如加入自动驾驶技术)。特斯拉计划在未来几年大大提高其汽车销量,因此从脸谱网和优步的增长团队招募了一批人才,并宣布:“我们正在从零开始打造一支增长团队,以设计、构建并优化可扩展的加速用户增长的解决方案。”

    解决速度需求

    增长黑客方法也可以解决当下所有企业都面临的速度难题。在当今竞争空前激烈且日新月异的商业环境中,迅速找到增长方法是至关重要的。增长黑客方法通过革新开发与发布产品的传统过程,设立持续的市场试验制度,系统性地实时应对市场需求,能够让企业抓住新机遇并且迅速解决问题,使企业增长更加迅速。这使得采用此方法的企业能够获得强有力的竞争优势,而且随着企业发展步伐的不断加快,这一优势将愈益凸显。

    现在的企业需要能够灵活并迅速地适应新技术与新平台,这至关重要。按照现在大部分企业仍在推行的传统商业模式,产品管理、市场营销、销售和工程等部门像一个个筒仓一样相互割裂,它们有着各自的任务计划,跨职能互动十分有限。产品团队负责开展市场调研、制定产品规格并评估市场规模。当完成设计后,产品被交付给工程部门或者生产部门,之后成品又被交回到产品部门手中。与此同时,市场部在收到产品部的市场调研结果和产品说明之后便着手制订营销方案,营销人员经常与第三方机构合作制订广告与推广计划,这就意味着这一工作往往远离核心员工。当产品开始发售时,公司才会想方设法扩大销量,实地销售报告也会反馈到产品和市场团队,以便为下一次产品发布提供指导。这一非常低效的循环可能需要几个季度甚至几年时间才能完成,使得企业无法及时适应消费者不断变化的需求或者技术的发展,也无法及时进行新的能力建设、产品改进和营销渠道拓展以吸引新客户。

    换言之,无论是创业公司还是成熟企业都承受不起被组织中的筒仓结构拖累的代价。通过打破这些壁垒,增长黑客方法能够使团队和企业更灵活、更迅速地适应不断变化的市场需求,加速推出新产品和新功能,也加速制定并实施能够吸引、激活新客户并从中变现的营销和销售策略。正因为这种对于速度的要求,增长黑客法的一个关键特征便是以尽可能快的节奏进行试验。正如脸谱网的增长副总裁亚力克斯·舒尔茨所言,“如果你每两周推送一次代码,而你的竞争对手每周推送一次,这意味着在短短两个月之后,你的竞争对手将会完成10倍于你的试验量。他们学习到的与产品相关的东西也将是你的10倍。”

    前言:低成本、高效率的精准营销(下)

    挖掘数据“金矿”

    增长黑客给公司带来竞争优势的另一个途径是帮助公司充分利用海量的客户数据。如今,在新技术工具的帮助下,企业能够更容易地收集这些数据,而这些数据蕴藏着等待开采的增长“金矿”。然而,对于今天的企业,无论大小,要从这些数据的“高山”中搜索并提取宝贵的“金矿石”却并非易事。在大多数情况下,企业都没有探索出收集用户数据的综合性方法。产品经理可能会开展调查和试验,但这往往是在没有市场部门参与的情况下进行的,而市场部往往也会自行收集数据并自己使用,不与其他团队分享。企业往往会聘请广告公司进行市场推广,广告公司也会收集相应数据,但是事前也不会咨询其他部门对于信息收集范围的意见。同时,编程团队收到的都是基于过去的数据提出的要求,而这些数据反映的客户需求往往已经过时了。

    结果,企业要么是以错误的数据指导行动,依赖的是肤浅、无用的指标(比如页面访问量),要么由于内部过于分化而错过宝贵的增长思路和机会。增长黑客能够帮助企业更有效地利用数据,以提取具体的、有意义的且实时的用户行为洞察,企业则可以利用这些洞察制定相应的战略,提出更有效且更有针对性的增长策略。

    沃尔玛的移动应用“低价捕手”(Savings Catcher)就是一个很好的例子。这个应用产生于与沃尔玛的价格匹配政策相关的用户行为分析。为充分利用广告匹配(即零售商同意为某一商品匹配市场上的最低价格)的兴起,沃尔玛的增长团队请工程师开发了一款 App(应用软件),使客户能够通过手机拍照上传沃尔玛购物小票,如果其他连锁超市对其中任意商品提供更低的价格,客户就可以自动获得沃尔玛的返现。此外,工程团队发现,他们可以将沃尔玛在其价格匹配项目中收集的数据与他们外包的调查团队进行的广告推广计划结合起来,这样沃尔玛就可以只对它具有明显价格优势的商品进行大力推广,从而大大节省了广告开支。

    沃尔玛前市场副总裁布莱恩 · 莫纳汉认识到沃尔玛最大的财富正是它的数据,因此他推动公司各个部门的数据平台进行整合,使工程部、产品推广部、市场营销部甚至第三方代理商和供货商都能够通过一个统一的平台利用其产生并收集的数据。增长黑客通过促进合作和信息共享使大数据的作用最大化。莫纳汉强调,企业运营离不开这一方法:“你需要能够理解软件编写过程的市场人员、重视消费者洞察并理解商业问题的数据科学家。”

    传统营销成本上升而回报却不明晰

    传统营销手段如平面广告和电视广告,也包括近些年兴起的网络广告,都已陷入危机。随着市场越来越碎片化、变化速度越来越快,传统广告出现了成本不断上升而关注度却不断下降的局面。一个主要的问题,是在欧美等主要市场,互联网用户的增长已经停滞不前,现在,互联网已经覆盖美国89%的人口和英国93%的人口,互联网用户的增长几乎像这些国家的人口增长一样缓慢。即便在增长迅速的移动互联网领域,美国的移动用户普及率也已经达到了64%。这说明随着广告支出继续向线上转移,在访客量相同的情况下,广告间的竞争会更加激烈,从而推动线上广告价格以惊人的速度不断拉升。

    同时,随着消费者对技术的了解日益加深,他们也更加排斥广告。事实上,在美国,有6980万互联网用户在使用广告屏蔽软件,其中近2/3是千禧一代的年轻人。另外,已经有50%的美国家庭订购了网飞(Netflix)、Hulu(一个免费视频网站)和 Amazon Prime(亚马逊金牌服务)等视频服务,更别说还有 TiVo 等 DVR(数字硬盘录像机)技术,“看电视”这个概念(包括电视广告)就像20世纪50年代的斯旺森电视晚餐(冷冻快餐)一样已经过时了。简而言之,电视广告已经变成了一种噪声,甚至完全被人们忽视或屏蔽。

    这一传统营销的危机有多严重呢?最近麦肯锡发布的关于上市软件公司的研究报告指出,营销投资和增长率之间没有任何相关性,完全没有。富尔奈斯营销集团(Fournaise Marketing Group)关于 CEO 对传统营销看法的研究发现,“73%的 CEO 认为市场营销人员缺乏业务可信度,对效果的关注不足”,“总是在要钱,却很少能够解释清这笔钱能带来多少额外的业务”。

    增长黑客使企业无须耗费资金开展过时、昂贵且商业价值模糊的营销活动便能取得爆发式增长。它依靠的是设计出让消费者爱上一个产品或一项服务并忍不住向朋友宣传的特性或功能,是提出富有创意的点子,以新的、可衡量的方式吸引用户。这种做法的威力是巨大的。

    抢抓新技术机遇

    消费者发现新内容和新产品的途径在发生着日新月异的变化,而下面这张图充分说明了这一现象。这张图是由风险投资家、增长专家詹姆斯 · 柯里尔制作的,体现的是不同数字营销渠道的发展趋势。在如今这个网络平台常常一夜之间兴起或衰落的世界,快人一步采用新技术和新网络平台对于企业获得增长先机来说至关重要。

    enter image description here

    前–1 病毒式传播渠道的有效性

    要抓住这些机遇,技术和营销团队需要紧密合作。然而大部分公司在利用新兴平台方面往往受到传统规划、预算制定和组织规章的束缚,行动太慢了。因此当这些公司准备好要采取行动时,它们早就已经丧失了先发优势,而且现在变化的节奏还在不断加快。

    打破传言

    在具体探讨增长团队是什么、怎么搭建之前,我们希望先澄清一些对于增长黑客的误解。首先,这一过程并不是像很多人以为的那样,要帮助企业找到一把“尚方宝剑”。媒体报道了很多广受称赞的增长黑客案例,比如 Dropbox 的用户推荐计划和 Airbnb 的 Craigslist 自动发布程序,这使很多人以为激发增长所需要的仅仅是一次伟大的增长黑客行动。不可否认,寻找像 Dropbox 的用户推荐计划这样的成功行动的确是增长黑客实施过程的目标,但是大多数情况下,增长是来自一次次小成功的积累。这些小成功就像是储蓄账户里的利息,通过一点点累加实现增长的腾飞。而且,在实现腾飞之后,优秀的增长团队仍会继续开展试验、进行改进。在本书中,我们也将介绍诸如脸谱网、领英、优步、Pinterest 和 Dropbox 等顶尖的增长团队是如何全力以赴不断提出、测试并完善新的增长黑客思路的。

    其次,很多公司认为它们可以直接聘请一个“独行侠”来做增长黑客,这个人有很多神奇的手段可以给公司带来更快的增长。这就大错特错了。这本书要说明的是,增长黑客其实强调的是团队合作,最伟大的成功案例都是来自编程技术、数据分析能力和营销经验的结合,而很少有人能够精通所有这些技能。

    增长黑客也经常被认为是通过设计巧妙的变通手段打破现有网站和社交平台规则的做法。虽然 Airbnb 广为流传的 Craigslist 案例很可能让人这么认为,但是打破规则绝非必要步骤,而且在大多数成功的增长案例中并没有这种做法。Airbnb 的做法的确是天才之作,但是这样的“旁门左道”并不是增长黑客的核心,而且大部分增长专家都对这个案例嗤之以鼻。Airbnb 的真正故事是它的创始人做了大量的试验来寻找增长点,但大部分试验都失败了,而后他们才想出了在 Craigslist 自动发布房源信息的主意,在成功之后他们仍然继续进行严格的试验并测试了很多完全合乎规则的策略以促进增长。

    当我(肖恩)提出“增长黑客”这个词来为这套方法命名时,我取的是这个词现在所具有的更广泛、更积极的含义,正如它在“黑客空间”(hack space)和“黑客马拉松”(hackathon)等词中的意思,以及脸谱网总部的地址中“黑客路1号”(1 Hacker Way)中的含义,即通过合作创造性地提出新想法,解决棘手的问题。而这正是增长黑客的关键特征。

    再次,人们常常认为增长黑客方法的任务就是吸引新用户或新客户。其实,增长团队应当承担更广泛的责任,事实也是如此。它们也需要关注客户激活,也就是说使原有的客户成为更活跃的用户和买家;也需要思考如何使他们成为产品或服务的宣传者。此外,增长团队应关注如何实现用户留存和变现,也就是说使客户成为“回头客”,增加他们为公司创造的收入,从而实现长期持续增长。通常,企业过于关注如何获取新用户和新客户,而这些新用户和新客户往往很快就失去了对企业的兴趣。企业在这方面浪费了太多资金。比如,Econsultancy(数字营销和电子商务咨询公司)2012年的报告指出,在获取更多网络流量方面,每花费92美元,只有1美元将访客转化成实际使用者。

    客户失去兴趣并离开,这种情况发生在网站访客身上时通常称为“跳出”(bounce),而发生在付费客户身上时叫作“流失”(churn)。这正是初创企业和成熟公司面临的最大问题,同样也是能够直接带来增长的机会所在。

    还有人认为增长黑客方法只是一种市场营销。但是正如之前所讲,增长团队也应参与到新产品开发中去,以分析一个产品是否针对目标市场进行了优化,它是否可以提供给用户所谓“不可或缺”的体验,以及它是否能够为合适的用户群提供这一体验。这就是我们常说的产品—市场匹配。在这之后,他们也需要提出丰富的想法以继续推动产品改进,同时对不同的想法排定优先级并进行试验,以确定哪些想法能够带来收入并驱动增长。增长团队甚至也可以在企业的发展战略方面发挥作用。例如,脸谱网的增长团队促成了公司一些战略性收购(如对 Octazen 的收购)从而推动公司增长。Octazen 提供用户任意邮箱通讯录的导入服务。事实上,正是脸谱网的增长团队最早意识到 Octazen 的技术能够使脸谱网的用户更轻松地邀请联系人加入这一社交网络。

    总而言之,增长团队应当参与增长的各个阶段和各个方面的努力,无论是实现产品—市场匹配,还是用户获取、激活、留存和变现。至于具体做法,我们将在正文详细说明。

    本书结构

    我们将本书分成了两个部分。第一部分为“方法”,整体介绍增长黑客方法的实施过程,比如如何搭建增长团队,团队需要哪些人、哪些技能,人员如何管理,以及团队如何通过快节奏的增长黑客过程产生并测试新想法,而这一过程又是如何迅速产生强有力的效果的。我们将介绍我(肖恩)和其他增长团队领导者摸索出的这个十分有效的过程。这一过程能够促进顺畅的跨部门合作,以创造增长动力、破解增长奥秘,而且它能够毫不费力地适应并满足任何企业的不同需求。总之,第一部分将阐述增长黑客方法的种种细节,并说明为什么企业应当推行这一方法。

    第二部分为“实战”,介绍具体实施这一方法的一整套详细策略,并分章节阐述如何实现用户的获取、激活、留存和变现,以及如何在增长实现之后维持并加速增长。我们将在这一部分分享来自不同行业、不同公司的增长故事,包括像 Pinterest 和推特这样的独角兽公司,像 Spotify(正版流媒体音乐服务平台)和印象笔记这样的移动应用,像 Hubspot 和 Salesforce.com(一家客户关系管理软件服务提供商)这样的商业软件公司,像好订网(Hotels.com)和 Zillow(提供免费房地产估价服务的网站)这样的网络门户,还有像亚马逊和 Etsy 这样的电商平台,以及像沃尔玛和一家我们虚构的食品百货连锁店这样的实体零售商。我们将介绍它们的增长团队是如何利用不同的增长黑客方法驱动增长的。另外,我们也会介绍一些供增长团队使用的网络工具,比如 GrowthHackers.com 的“项目”软件,这些工具可以帮助增长团队管理本书第一部分介绍的增长过程。同时,我们也会介绍一些客户调查工具,提供排定试验优先级并记录试验结果的模板,说明增长会议的组织方式,也会针对不同关注点建议一些可以开展的试验,同时,GrowthHackers.com 也会不断更新这些信息。

    世界各地不同行业、不同形态、不同规模的公司都在谋求发展出路的过程中面临着巨大的挑战。而增长黑客法提供了一套严密的方法论,帮助企业通过跨职能合作迅速发现增长机遇。它强调以数据驱动的分析和试验。企业在收集数据方面往往投入巨大,而这样的分析和试验能够以系统性的方式帮助企业发挥海量数据的强大作用。我们将通过这本书说明,所有类型的企业都可以实施这些策略,不论是先小范围实施还是要在整个公司层面推行。增长黑客是一个全新的、颠覆性的商业方法论,任何企业、任何公司创始人、任何公司团队领导者,如果他们希望不负众望,希望取得重要成果,希望以有限的营销投入获得最大的回报,希望实现企业目标,就必须采用这个方法论。我们接下来将说明如何实施此方法论。

    第01章:搭建增长团队(上)

    2012年,当普拉莫德 · 索克加入 BitTorrent 成为其新任产品总监时,这家一度发展得如火如荼的创业公司正处于一个岔路口。它提供的计算机软件使用户可以从整个网络搜索并下载文件,但当时软件的增长陷入了停滞。更令人担忧的是,它当时还没有推出产品的移动版,在人们迅速从 PC(台式电脑)端转移到移动端的时期,这无疑是一个巨大的劣势。更糟糕的是,YouTube、网飞等流媒体服务在手机和其他设备上占据着用户越来越多的时间和注意力,BitTorrent 则被甩在了后面。普拉莫德的加入便是为了打造移动版产品,重启增长。

    这家50人组成的公司采用的是传统的筒仓式组织架构,分为市场部、产品部、工程部和数据科学部。产品团队和工程团队下设小组,负责不同的产品,比如 Mac 版和 Windows 版客户端以及刚刚起步的移动版。跟所有类型的企业一样,它的数据团队和市场团队均为这些产品小组服务,产品开发过程完全与市场营销相分离。产品经理会告知市场经理即将发布的新产品或新版本,之后市场团队便负责所有的营销工作,也就是说营销工作完全没有产品研发人员的参与。

    像很多公司一样,BitTorrent 的市场团队也只关注这个漏斗的顶端(如图1–1),即提高产品知名度,通过品牌建设、广告和网络营销吸引更多新用户。在大多数软件公司或基于网络的公司,提高网站或 App 访客的激活和留存数量并不是由市场人员负责,而是由产品和工程团队负责,他们致力于开发能让用户爱上产品的功能。而这两个群体之间几乎不存在任何合作,他们各自专注于各自的重点工作,几乎没有什么互动。有时候他们甚至都不在一栋楼里,甚至不在一个国家办公。

    enter image description here

    图1-1 客户漏斗与典型部门权责划分

    按照这样的组织分工,当 BitTorrent 的移动应用开发完成之后,市场团队制定了一个发布方案,像往常一样,这个方案包含了一系列传统的营销活动,并且重点放在了社交媒体、公关和付费获客推广上。App 本身做得很好,营销方案也不错,但是用户增长仍然非常缓慢。

    普拉莫德决定让营销团队招募一个专门的产品营销经理(PMM)来帮助促进用户增长。产品营销经理经常被称为公司内部“客户的诉求传达者”,他们致力于了解客户的需求和喜好,常常开展客户访问、调查或关注群体动向,帮助改进信息传达方式,以使营销活动更具吸引力,并能更有效地传达产品的价值。有的公司也会让产品营销经理参与产品开发,比如通过开展调研发现值得考虑的新功能,有时也会让他们协助产品测试。

    安娜贝尔 · 萨特菲尔德是一位经验丰富的产品营销经理,她加入了 BirTorrent 的市场团队协助新的移动 App 的用户增长。除了关注产品知名度和获客工作外,她还要求和产品团队一起合作,从漏斗的其他环节(包括用户留存和变现)驱动增长,而不是仅仅将工作局限在漏斗的顶端。市场总监同意了她的要求,但前提是她必须先专心开展获客计划并且达成市场团队制定的目标。

    然而在做了一些市场调研和用户行为数据分析之后,她发现了与市场总监的要求相矛盾的情况:很多最佳的增长机会似乎都蕴藏在漏斗的下游。例如,她了解到很多 App 用户都没有升级到付费的专业版,于是她做了一项调查,询问那些没有升级的用户为什么不升级。如果团队能够使更多用户升级到专业版,这将给公司带来可观的收入,而这可能比吸引更多用户下载 App 更重要。在看到调查结果之后,她清楚地意识到最有可能成功的增长策略不是只关注用户群的扩大,也要最大限度地发挥现有用户的价值。

    她把这一发现告诉了产品团队,心想他们或许可以一起寻找改进 App 的方式。但是她的这一举动让产品团队有些措手不及,因为这是头一次有市场人员给他们提供这样的信息。但是普拉莫德非常满意,他坚信应当以数据驱动的方式进行产品开发。他马上告诉安娜贝尔放手去做,继续从用户调研中获取产品洞察,并越过组织框架将这些洞察传递给产品团队。

    安娜贝尔的一个调研发现让产品团队十分惊讶,而且给公司带来了迅速的收入增长。产品团队对于为何很多用户没有升级到专业版 App 有很多猜测,但是看到用户对安娜贝尔提出的问题的回复之后,他们大吃一惊。出现频率最高的回答是什么呢?是用户完全不知道专业版的存在。产品团队感到难以置信,他们以为他们已经对免费版的用户进行了大力的专业版推广,但显然他们失败了,连最活跃的用户都没有注意到专业版的存在。于是团队马上决定在 App 的主屏上增加一个非常突出的升级按钮,不可思议的是,这一个简单的改变就使每日升级收入获得了92%的增长。这个改变的成本几乎为零,执行起来几乎不需要时间(从讨论调研数据到部署按钮只用了几天时间),却马上带来了显著的成效。如果没有用户的反馈,他们也许永远都想不出这个办法。

    另一个成果也值得一提,安娜贝尔和普拉莫德称之为“爱的增长法”(love hack)。为寻找每日 App 下载量上升或下降的原因,安娜贝尔对用户数据进行分析,从中发现了一个明显的规律。他们的 App 只能在谷歌商店里下载,她发现每当商店里 App 评论区排在前面的是负面评价时,日下载量就会下降。她试着将好评置顶,马上就看到了下载量的增加。于是她和普拉莫德决定在用户下载第一个种子文件后就鼓励他们写评论,因为这时他们已经了解到这个 App 是多么好用,而且他们二人也认为这应该是用户对 App 最满意也最有可能给好评的时候。他们要求工程师编写了一个请求代码,在用户下载了第一个种子文件后请求就会以弹窗的形式出现,然后他们做了一个试验来验证这个假设。可想而知,好评开始如潮水般涌来。基于这个试验的结果,他们开始向所有新用户推送这个请求弹窗,结果,四星和五星好评数增加了900%,下载量也大幅上升。安娜贝尔也由此建立起了她的威信,不久之后就有工程师主动找到她询问:“你还有其他想法吗?我们还可以做些什么?”

    遗憾的是,市场和产品团队很少能够像这样通力合作。通常情况下,产品团队负责打造产品,也负责产品更新,比如改进注册体验或增加新功能。团队会确定一个产品改进的时间表,通常称为“路线图”。产品改进的某个想法如果没有包含在事先确定的路线图里,往往在推进的时候就会受阻。有时是因为要完成计划中的产品改进所需要的时间已经非常紧张了,有时是因为提出的想法太粗糙,难度太大,也太费时间,因此成本太高,而提出想法的人并没有考虑到这些因素。有的情况下,产品团队也可能认为提出的要求与产品战略愿景不相符(也可能是上述不同原因的叠加)。

    即使你不在技术公司工作,你可能也十分了解不同部门之间的这种紧张关系,比如市场团队不理会销售人员的建议,或者研发团队拒绝市场团队提出的设计新产品原型的提议。这是以部门划分职责的筒仓式结构的主要问题之一,也是为什么增长团队必须包括不同专业、不同部门的人员,我们稍后也会详细说明这一点。正如 BitTorrent 团队很快认识到的,最好的想法往往产生于这种跨职能的合作,这也是为何跨职能合作是增长黑客过程的一个根本特征。

    打破筒仓

    接连不断的成功让 BitTorrent 移动团队大受鼓舞,每一个人都开始提出可以测试的想法。他们测试的其中一个想法只有那些拥有无私精神的技术人员才能想到:自动关闭应用以节省手机电量。移动团队在对免费版 App 的“超级用户”(那些频繁使用 App 但还没有升级到专业版的人)进行专项调查的时候发现了这个增长机会。调查发现这些用户的一个主要痛点是由于高强度使用造成的手机电量迅速流失。于是,工程师很快提出了一个想法:打造一个专属于专业版的功能,当 App 检测到用户手机电量只剩不到35%时便自动关闭耗电的后台文件传输。他们在免费版 App 里推广这一功能,当用户电量不足时,App 便会提示他们专业版提供这个功能,吸引他们马上升级。这个新功能十分受用户欢迎,也使公司收入增加了47%。

    接二连三的成功也在整个公司流传开来。安娜贝尔正式从市场部调到了移动团队,直接向普拉莫德汇报工作,而且她的头衔后来也改成了高级产品增长经理。同时,其他项目的工程师也惊讶于移动团队的一次次成功,甚至有两位资深工程师为了加入这个以增长为导向的、高效运转的团队而离开了他们本来的团队。安娜贝尔说:“通过跟我们的工程师交谈,我们发现这不仅是因为我们的团队看起来很团结友爱,更重要的是他们认同我们这种数据驱动的方式。”

    随着团队不断取得成功,他们也越来越重视数据分析(数据分析工作由数据团队的一个同事负责)。他们依靠数据分析开展试验并分析试验结果。数据分析师和工程师一起共事,确保他们在分析用户对试验的反应时跟踪的是合适的、有用的数据,并根据这些数据提供最有意义的报告。数据分析师掌握足够的技能,他知道什么时候能够判断试验的成败。之后他便与团队协作,分析试验结果并计划后续试验。随着团队对数据分析的依赖不断加深,最后这位分析师也像安娜贝尔一样作为全职员工加入了团队。

    这一以数据驱动增长和产品开发的方式取得的巨大成功促使 BitTorrent 的高管决定加大对数据科学的投资并招募更多数据分析人才。同时,随着移动团队增长故事的流传,其他产品团队也开始更频繁地采用数据分析,也更紧密地和数据团队合作开展试验,寻找增长思路。

    移动团队后来又提出了其他几十个具有重大影响力的产品改进创意,在实施增长黑客方法两年半的时间里使产品下载量达到了1亿次。在移动 App 任务完成后,公司对团队进行了重组,安排他们负责公司其他重要的产品。这一支小小的团队给这个之前增长遇阻的公司带来了不容低估的影响。他们的工作不仅使 BitTorrent 的总收入在一年内增加了300%,或许更为重要的是,这个团队彻底颠覆了 BitTorrent 的企业文化,使之从受传统营销和筒仓束缚的文化转变为一个合作型文化,无论是市场人员还是数据分析师,抑或是工程师和高管,每一个人都围绕着这个快节奏、强调合作的增长黑客过程开展工作。回忆起这一增长过程在整个公司的传播和蔓延,安娜贝尔说:“我最喜欢的两个时刻,一是看到我们的老技术主管在‘Palooza’(BitTorrent 对其定期举行的黑客马拉松的称呼)上提出一个增长试验建议的时候,二是跟以前的一个同事见面,他告诉我他想加入我的团队一起开展这个工作的时候。他现在也成了增长黑客的‘宣传大使’。”

    然而,这种合作方式在各种类型、各种规模的公司里都很罕见。公司的不同业务部门通常都是彼此孤立的筒仓,很少交流、共享信息或者开展合作。这种组织结构在过去多年来一直广受诟病。麦肯锡的一份报告指出,筒仓式结构最致命的一个问题就是它会放缓企业创新的步伐、阻碍增长。作为本书的两位作者,我们一致认为,“对于创新力来说,在系统内部开展合作的能力比个人才能更加重要”。但是麦肯锡的一份调查发现,“虽然80%的高管都承认跨越部门界限进行有效的知识分享对于增长而言至关重要,但只有25%的高管认为他们的公司做到了这一点”。

    同样地,哈佛商学院的一群教授进行了一项关于业务部门之间沟通的研究,他们在报告中指出,业务部门之间的互动之少令他们“十分诧异”。更令人惊讶的是,“在同一战略业务单元、职能或办公室的两个人比不同业务单元、职能或办公室的两个人互动的频率高1000倍。也就是说,跨越界限的互动少得可怜”。

    西北大学凯洛格管理学院的兰杰 · 古拉提教授是筒仓问题专家。他指出,部门之间缺乏沟通会阻碍以客户为中心的产品开发和营销——随着技术和社交媒体的推动,甚至要求企业持续不断地与客户进行更具实质性的互动,从而使以客户为中心的产品开发和营销方式显得越发重要。简言之,工程师和产品设计师有能力找到满足客户需求和喜好的方式,但是他们往往并不知道客户需要什么、想要什么。古拉提在他的一份高管调查报告中指出,2/3的高管都认识到未来10年以客户为中心的产品开发应当是重中之重,但是他的研究发现,“企业的知识和技能孤立地存在于这种组织筒仓中,不同部门的人员难以跨越这些内部界限、充分利用企业资源,以便为客户提供他们真正需要并愿意付费购买的产品”。

    搭建跨职能的增长团队能够打破这种壁垒。跨职能团队不仅能够促进并加速产品、工程、数据和市场部门之间的合作,并且能够激励团队成员更多地了解并理解其他成员的视角及他们的工作。建立增长团队或许是公司战略需要,或者是为了某个具体项目或计划的推进。那么,如何搭建增长团队呢?我们接下来将给出具体步骤。

    第01章:搭建增长团队(中)

    人员构成

    增长团队里应当有对企业战略和目标有深刻了解的人,有能够进行数据分析的人,也要有能够对产品的设计、功能或营销方式进行改动并通过编程测试这些改动的工程师。当然,不同公司、不同产品的增长团队具体构成也不同。团队的规模各异,因此职责范围也各不相同。它可以是四五个人组成的小团队,也可以像领英的团队那样有上百个人。不论规模大小,增长团队都应当由以下角色构成(不一定包括下列所有角色)。

    增长负责人

    每一个增长团队都需要一个领导者,就像部队里的营长,既管理团队又能脚踏实地地参与到想法的提出和试验过程中。增长负责人会确定试验的流程和节奏,并监测团队是否完成了目标任务。增长团队一般每周开一次例会,由增长负责人主持会议(我们稍后也会说明如何组织会议)。

    不论专业或背景如何,增长负责人在团队里都扮演着管理者兼产品负责人兼数据科学家的角色。他们的一个主要职责就是选定核心关注点以及团队的工作目标和时间表。我们在接下来几章也会充分阐明,围绕一个主要目标开展试验对于优化结果来说至关重要。增长负责人可以确定一个月度、季度甚至年度关注点,比如使更多用户从免费版产品升级到高级版,或者明确某个产品的新营销渠道中哪一个是最优的。之后增长负责人就要确保团队在这个轨道上开展工作,而不会盲目跟进对既定目标没有意义的想法。对于这些想法,应存档留到目标调整时使用,届时,这些想法就可以用于新目标的实现。

    增长负责人也要确保团队采用适合既定的增长目标的指标来衡量并改进结果。经常出现的情况是,市场和产品团队没有系统性地跟踪关键用户行为数据,而这样的数据能够帮助企业发现有益的改进思路,或者能够使企业发现用户活跃度下降或者完全弃用产品的早期迹象。很多公司把太多的注意力放在纸面上很好看但并不能体现用户或收入真正增长的“面子指标”上,比如网站访问量。我们将在第三章详细介绍如何选择合适的指标。

    所有的增长负责人都应具备一些基本的技能:能够熟练进行数据分析、精通或熟悉产品管理(即开发与发布产品的过程),以及了解如何设计并开展试验。每一个增长负责人也都必须熟知促进用户增长的方法以及团队所负责产品或服务的用法。例如,一个社交网络的增长负责人应当了解病毒式的口碑效应和网络效应(即加入的人数越多,社交网络的价值越大)的原理,许多社交产品正是依靠这两个效应发展起来的。增长负责人也应当有相关的行业或产品知识,比如,一家网上零售商的增长负责人应该对购物车优化、产品推广、定价和营销策略有敏锐的把握。出色的领导力也是必要的,它能够使团队专注于目标任务,即使面对一次又一次的试验失败(这再正常不过了)时,增长负责人也应能激励团队加快试验节奏。走进死胡同、试验没有定论或者彻头彻尾的失败都是增长试验的常态。但是一个优秀的增长负责人能够使团队保持热情,而且能够为团队继续开展试验保驾护航,使团队在试验失败时不必遭受不必要的审查或者来自管理层的压力。

    增长负责人这一角色并不需要最佳职业背景。现在有的人以此为专业,但大多数人都是从工程、产品管理、数据科学或市场营销等其他专业领域转到这一岗位的。在上述任一领域享有专长的人都可能是不错的增长负责人候选人,因为他们都能够在增长黑客过程中发挥各自的优势。对于创业公司,尤其是创业初期的公司来说,创始人常常要扮演增长负责人的角色。也许有的创始人不直接管理增长团队,但是他们也应当任命一个增长负责人并且让他/她直接向自己汇报。在规模较大的公司,可能存在一个或多个增长团队,那么增长负责人应当由掌管团队工作的高管任命,并向该高管汇报。

    增长负责人的工作也许听上去令人生畏且任务太过繁重,但是我们将在接下来的几章介绍一些用于排定试验优先级、跟踪和分析试验结果的工具和方法,在掌握了这些工具和方法之后,这个过程便能够得到高效的管理。

    产品经理

    每个公司产品开发团队的组织方式各有不同,这会影响派到增长团队的开发人员,也可能会决定增长团队在公司组织架构里的位置,我们稍后将阐述这个问题。通常来说,产品经理负责监督产品及其功能的实现过程。正如风险投资家本 · 霍洛维茨所言,“一个好的产品经理就是产品的 CEO”。

    产品经理的角色其实起源于消费品行业,最早由宝洁设立。最初,这个角色的名称是“品牌经理”,直到今天很多公司仍在使用这一名称。这个职位通常属于市场部门,因为其职责就是帮助公司更好地理解并满足客户的需求。正如产品管理专家马丁 · 埃里克森所写的那样,这个角色将企业决策尽可能与客户拉近,使产品经理成为公司内部客户诉求的传达者。

    由于企业规模不同,产品经理的职能可能由其他员工负责,而在创业公司,特别是初创的公司,这一角色可能由创始人承担。但是在规模更大的公司,产品管理内部可能会分为几层,从产品经理、产品总监到产品副总裁或首席产品官。而派驻到增长团队的产品管理人员的层级可能各有不同,但是在很多软件公司,往往由增长团队所负责的产品对应的产品经理加入团队,并向产品总负责人汇报,而这个总负责人经常是产品副总裁。我们稍后会进行更详细的阐述。

    软件工程师

    为产品功能、移动界面和网页写代码的人可以说是增长团队的主力。然而他们往往被排除在构想过程之外,因为他们通常忙于公司新产品或新功能的开发。或者他们只是一味地听命于产品和业务团队,落实他们提出的任何想法。这不仅会削弱公司最有能力也是最宝贵人才的士气,也会阻碍想法的提出,因为这使工程师的创造力和在新技术方面的专长无法得以发挥,从而可能错过增长良机。前面讲到,在 BitTorrent,工程师提出的开发节电功能的想法带来了无法估量的价值。增长黑客的精髓就是源自软件开发和设计的黑客精神,也就是利用新的技术手段解决问题,如果没有软件工程师的参与,增长团队就不会完整。

    营销专员

    当然,有一些增长团队是没有专门的营销人员的,但是我们认为营销专员的参与能够帮助团队取得最优结果。工程和营销这两个专业之间的碰撞能够极大地激发新思路的产生。基于不同类型的企业或者产品,团队所需要的营销能力也各不相同。例如,一个致力于扩大读者群的内容增长团队显然需要一个内容营销专员。比如在摩根 · 布朗担任 COO(首席运营官)的英曼(Inman,一家房地产行业杂志社),增长团队里有一位邮件营销总监,因为邮件营销是这家公司客户获取、留存和变现的主要渠道。而有的公司可能更依赖搜索引擎优化,所以会有这个领域的一位营销专员加入增长团队。团队也有可能有几个营销专员,分别负责不同的领域。营销人员也可能只是在短时间内加入团队开展他们所擅长的工作,在目标实现之后便离开。

    数据分析师

    增长团队的另一个必要技能是精通客户数据的收集、整理与深入精细分析,并从中汲取试验灵感。团队里不一定要有全职分析师,可以有一位专门的分析师在处理公司其他工作的同时与增长团队合作。BitTorrent 的增长团队一开始就是这样。但是如果公司有能力聘任一位全职分析师,当然再好不过了。

    团队的数据分析师需要懂得如何确保试验的设计严密且在统计上有效,懂得如何获取不同来源的客户和业务数据并将这些数据结合在一起分析用户行为,并且能够迅速整理试验结果并从中提取结论。团队所开展的试验的复杂程度不同,因而在有的情况下这一角色可以由市场或工程团队的人担任,因为这两个领域都涉及一定程度的数据分析工作。而技术性更强的公司则同时需要一位擅长试验报告的分析师和一位擅长深挖数据的数据科学家。

    最基本的一点是,数据分析工作不能交给只知道用谷歌分析(Google Analytics)的实习生或者外包给数字代理商,这是比较极端的情况,但仍然普遍存在。我们将在第三章详细讨论这个问题。太多的公司不够重视数据分析,而是采用像谷歌分析这种预先设定的程序,导致它们无法有效地整合不同来源的数据,比如销售数据和客户服务数据,也使它们无法充分利用这些数据获得重要的洞察。数据分析师的能力大小将决定增长团队是在浪费时间还是在挖掘数据金矿。

    产品设计师

    同样,这个职位在不同类型的公司也有着不同的头衔和职能。在软件开发领域,专注于用户体验的设计师负责开发与用户交互的界面和序列。在制造业,设计师可能负责产品画图和规格。而在其他一些公司,设计师可能主要负责广告和推广文案的图像设计。在增长团队中,设计师的参与可以提高试验执行的速度,因为这意味着团队有一位专职人员能够迅速地完成任何需要设计的工作。用户体验设计师也能够在用户心理、界面设计和用户调研技巧方面提供重要的见解,帮助团队寻找试验思路。

    团队规模与工作范畴

    在创业公司和小型企业,增长团队中上述每个领域可以只有一个人,甚至团队只有几个人,每个人负责不止一项工作。而在大公司,增长团队可能包括多位工程师、营销人员、数据分析师和设计师。增长团队的构成和职责,包括团队的规模、组织结构和具体的任务和重心,必须符合公司的需要。增长团队的工作范围可能比较宽泛,比如负责公司各个领域的增长业务,也可能非常具体,比如负责产品某个部分的改进,如购物车功能。有些增长团队是固定的组织单元,比如 Zillow 和推特的团队,而有些则是为特定任务成立的,比如新产品发布,目标完成之后便会解散。有些公司成立了多个增长团队,每个团队负责不同领域的工作,领英和 Pinterest 就是这样。Pinterest 有四个增长团队,分别负责用户获取、病毒式增长、用户参与和新用户激活。而有的公司只有一个增长团队,负责多项任务,比如脸谱网和优步。

    如果你刚刚着手成立一个增长团队,那么你可以从不同部门各抽调一两个员工,这样可以使团队迅速启动,团队的规模可以日后逐渐增扩,也可能另外设立新的团队。例如,IBM 成立增长团队的时候是为了扩大 Bluemix DevOps 产品(为工程师提供的软件开发包)的用户群,公司调了5个工程师和5个来自业务部门和市场部门的其他员工组成了团队。在英曼,摩根率领的增长团队由一个数据科学家、三个营销人员和一个网页开发者组成。摩根也是公司的产品开发负责人,他在团队里担任产品经理的角色,作为 COO,他也是团队里级别最高的成员,但是他并不是增长负责人,增长负责人由一位市场经理担任,摩根负责帮助并指导团队工作。

    工作流程

    选好了团队成员之后,他们应该做些什么呢?增长黑客过程提供了团队应当开展的一整套具体活动,通过迅速试验寻找新的增长机遇并扩大现有的机遇。这一过程是一个持续的循环,由四个主要步骤组成:(1)数据分析与洞察收集;(2)想法产生;(3)排定试验优先级;(4)试验执行。在第四步完成之后重新回到数据分析阶段,评估试验结果并决定下一步行动。在这一阶段,团队将确定产生了初步成果的试验想法,并进一步完善,而对于结果差强人意的试验想法,则直接抛弃。通过循环往复地推进这一过程,增长团队将不断积累大大小小的成功,创造一个不断改进的良性循环。

    enter image description here

    图1-2 增长黑客过程的四个步骤

    团队应定期召开增长会议以保证工作进度。团队会议一般应每周召开一次,它能够为管理团队试验工作、回顾试验结果并决定下一步试验内容提供一个严谨的场合。增长团队可以采用敏捷软件开发方法里的常设会议做法。敏捷开发利用冲刺计划会议组织下一步工作,而增长会议也十分类似,使团队能够回顾进展、确定下一步试验顺序并保持试验速度。

    在会议上提出的试验想法将分配给不同成员负责执行、分析或研究,以明确该想法是否值得试验。团队增长负责人在两次周会之间定期与每个团队成员进行沟通,检查工作进度,并帮助他们解决可能出现的问题。

    周会可以使团队保持工作进度与重心,确保高度的协调与沟通以推进这一节奏飞快的过程。这个过程就像是在赛道上飞驰的 F1 赛车,在保持速度的同时不断进行微调,如果做不好则可能变成刹车失灵冲出道路的笨重卡车。除此之外,这种深度合作的会议能够达到1+1=3的效果,不同成员的专长结合在一起的影响力将会翻倍,使优质的想法变成强大的增长引擎。这样的会议也经常能够催生非常出人意料的想法,而其中很多都是成员独自一人无论如何也想不到的。

    例如,通过对于客户流失的深入分析,团队可能会发现很多弃用产品的人都没有使用产品的某一功能,而这一功能非常受活跃用户的欢迎。这一发现可能促使团队试验不同的方法,引导用户试用这个功能。再举一个我们 GrowthHackers.com 增长团队的例子:在分析用户数据时,我们发现社区提交的内容包含富媒体格式(比如会议演示文稿或者 YouTube 上的视频),与简单的文章链接相比,这些内容大大提高了用户参与度并为网站带来了更多的重复访问量。于是增长团队成员提出了一系列为网站增加更多音频、视频等富媒体内容的想法。接下来的工作看似很明确但缺乏新意,直到团队里的一位工程师加入进来,告诉我们不仅可以用一个简单的插件在网站上支持很多类型的媒体文件,还可以嵌入一段代码,自动识别 YouTube、SoundCloud 和 SlideShare 等知名媒体网站的链接并马上将链接内容嵌入网站的讨论页面。于是我们所做的就不是直接增加一两个媒体源的视频那么简单了,而是实现了对十几个媒体源内容的支持,而且大大简化了在网站上发布多媒体内容的过程。如果没有工程师的参与,我们很可能不会想到这个办法。之后,我们重新设计了试验,结果网站社区活跃度的增长远远超出了我们的预测。我们将在第四章介绍最高效的会议组织流程,也会提供一个会议日程样板供读者参考。

    人员分工

    至于人员分工,每个团队成员仍然应负责各自擅长的工作,有时也需要独立工作,至少在团队成立初期是如此。例如,工程师会负责试验所需要的编程工作,设计师负责团队需要的任何设计元素,数据分析师则负责挑选试验将要覆盖的用户群,而营销人员则负责执行任何针对营销渠道的试验,比如新的脸谱网广告计划。如果团队里有用户体验设计师,他/她可能会负责收集并分析用户关于他们最看重的产品功能的反馈,并将分析结果汇报给团队。而这样的调研可能会引导团队提出改变某个功能或者测试某个新功能的想法。而后,团队可能会让工程师负责相应的编程工作,比如,如果调研发现用户在浏览网站时容易“找不着北”,工程师就可能需要对网页进行调整,比如修改购物车界面。

    当然,团队成员在其他时候也需要密切合作。比如要开发一个新的产品功能时,团队负责不同工作的成员需要就产品的设计和执行、信息传达方式和产品提供方式以及结果评价指标等达成统一意见。例如,负责移动 App 的团队可能会通过讨论决定,将提高新用户转化为固定用户的速度作为工作重心,也可能因此决定的需要对新用户首先看到的几屏进行大刀阔斧的重新设计,以优化页面的信息传达,并就此开展试验。

    第01章:搭建增长团队(下)

    必要的高层支持

    公司的组织汇报结构必须对增长团队有清晰的界定,即增长负责人应向谁汇报。团队应当由一位高管负责,以确保团队有权跨过既定的部门职责界限开展工作。增长不应作为一个边缘项目存在,如果没有明确且坚定的高层意愿,增长团队将会在公司中处处受阻,陷入低效、僵化的形式主义和地盘之争。在创业公司,如果创始人或 CEO 本人不直接领导增长团队,那么团队就应该直接向他/她汇报。在规模更大的公司,可能同时有几个增长团队,团队则应向副总裁或者首席官中的一位汇报,这位首席官能够保证整个首席官团队都支持增长团队的工作。总之,组织最高层的支持对于团队取得持续的成功至关重要。

    马克 · 扎克伯格就是增长团队所需领导力的典范。他在脸谱网成立初期就不遗余力地推动增长,从那时起他对增长工作的热情一直不曾消退。2005年,也就是脸谱网正式成立增长团队两年之前,公司负责数字营销的诺亚 · 卡根向扎克伯格提出了一个能够为公司创收的想法。卡根是第三十名加入脸谱网的员工,他认为公司需要向投资者证明他们是可以盈利的。当时,他们在公司的会议室里,卡根正在阐述他的想法,而扎克伯格打断了他,从座位上起身,走到白板前拿起一支记号笔在上面写下了大大的“Growth”(增长)一词。扎克伯格认为,公司当时需要做的就是全力以赴扩大脸谱网的用户人数,不应该有别的想法。他在公司创立初期就态度鲜明地把增长置于包括收入在内的所有其他企业关注点之上,这正是脸谱网取得巨大成功的关键所在。

    即使在企业纷纷投资虚拟现实和人工智能等未来技术的今天,核心客户群的稳健仍然毫无疑问是投资未来的基础。正如脸谱网的 CTO(首席技术官)迈克 · 斯科洛普夫对《快速公司》杂志(Fast Company)所言,“我一半时间在关注公司日常运营,一半时间在关注未来发展,这有时会让人有些错乱,但是我们必须要保证核心业务保持持续增长,因为这是我们大力投资长期项目的前提和基础”。

    世界最大的房地产网站 Zillow 的联合创始人斯宾塞 · 拉斯科夫也是增长过程的坚定拥趸。负责公司增长业务的副总裁奈特 · 默克是公司的第四十位员工,他回忆说,拉斯科夫和高管团队从第一天起就把增长作为首要任务。随着公司的成长,Zillow 也建立起了一支以默克为中心的增长团队,以保证公司始终如一地专注于增长。默克的团队与脸谱网的工作模式类似,主要关注公司的 KPI(关键绩效指标),并与其他产品团队合作促进获客和留存,以实现各自的业务目标。

    拉斯科夫设立了一个名为“Play”的增长计划,即以每9到12个月为一个周期不断进行的增长活动,并以此为核心将整个公司凝聚起来,共同实现增长使命。例如,在2008年,公司注意到其网站流量正在被一个竞争对手吞食,这个竞争对手是一家叫 Trulia(美国房地产搜索引擎)的行业新秀。流量的流失在很大程度上是因为 Trulia 巧妙地利用了搜索引擎优化使其在谷歌搜索结果中的房源数据高于 Zillow。于是 Zillow 的高管团队决定将当年“Play”计划的主题定为搜索引擎优化,公司里的所有团队都要将此作为首要任务,力争在搜索方面做到世界一流。这意味着公司文化的重大改变,因为在此之前公司一直忽视了搜索引擎优化,而更多地关注其他营销策略。但是最终每个团队都找到了搜索引擎优化的办法,而且 Zillow 也赶超了 Trulia,并在2015年以30亿美元将其收购。

    汇报结构

    研究员安德鲁 · 麦金尼斯和三善大辅开展的针对硅谷企业的调查显示,增长团队有两种常见的汇报结构。他们将第一种结构称为职能模式(或产品部门主导模式),在这一模式下,增长团队向主管其工作范围内的某个产品或几个产品的高管汇报。

    enter image description here

    图1-3 产品部门主导模式

    比如,产品部门主导的团队可能将全部精力放在公司移动 App 用户的增长上,而另一个团队则可能专注于推动接受网络免费新闻资讯服务的读者升级到付费订阅的行列。有些情况下,产品部门主导团队的职责范围可能仅限于改进产品某一个方面的性能,比如通过优化 onboarding(即引导用户了解如何使用产品)激活某线上学习软件的新用户。在 Pinterest,约翰 · 伊根领导的增长团队全心贯注于测试发给用户的邮件和手机推送消息的频率、内容和行为召唤,旨在促使用户更频繁地使用产品。这一职责范围听起来可能太小了,但是正是这种高度的专注才能使团队真正深挖公司的关键增长点。例如,团队最近的一次尝试是打造了“Copytune”,这是一个非常复杂的机器学习算法,使团队能够快速地测试用30多种语言编写并推送给用户的几十种不同的通知版本,在软件选出效果最好的版本之后,团队再跟进试验以寻求更优结果。这一计划的成效非常显著,使网站的月活跃用户数(MAU)大增。

    产品部门主导的团队也可能负责尝试各种不同方式以驱动增长漏斗各个层面的增长,从吸引新客户到提高留存率,再到增加客户产生的收入。

    一般来说,在采用这一模式的公司,每一个产品经理都会领导一个由工程师、用户体验设计师和数据分析师组成的小产品团队,而且经常一个产品部门会有若干个这样的小团队。这个模式在成熟的企业或者已经处于创业后期的公司比较容易实施,因为它能够很快地融入既有的管理结构。这不仅意味着企业架构不需要大幅重组,也能够减少在将增长试验纳入既有产品功能开发路线图的过程中可能遇到的摩擦。

    除 Pinterest 之外,领英、推特和 Dropbox 也采用这一模式。

    另外一种模式是创建一个独立的团队,这样的团队不再归属于既有的产品开发团队。增长团队负责人向增长副总裁汇报,而增长副总裁一般直接向 CEO 或者其他高管汇报。增长副总裁这个角色的设立通常是为了保证公司管理层中有人对团队的成果负责,优步和脸谱网就是两个例子。不同于产品部门领导的团队,独立团队并非只专注于某一个产品,而是有权就公司所有类型的产品开展试验,甚至可以在公司现有的产品线之外寻找战略层面的增长机遇。一个典型的例子就是脸谱网的增长团队在认识到 Octazen 的技术能够帮助改进其朋友推荐计划的效果时建议公司收购 Octazen。而且,脸谱网的增长团队开展的增长计划覆盖范围十分广泛,他们不仅帮助公司改进既有产品及功能,比如优化新用户注册流程,甚至还为公司开发了一些新产品,比如 Facebook Lite,这是专为数据通信比较落后的地区建立的。增长团队还为产品团队提供支持,当产品团队遇到困难时,增长团队会像特警部队一样空降过来帮助他们发现产品优化和增长的机会,并向他们展示如何推进增长试验过程。

    enter image description here

    图1-4 独立模式

    在公司发展初期成立独立增长团队的阻力是最小的,因为这时公司架构还没有成型,资源归属和汇报制度还没有正式确立下来。在地盘尚未正式划分的时候,如果要分配一部分职责和人员给增长团队,公司里不会有太多的抱怨声。不过虽说如此,在规模更大、更成熟的企业成立独立的增长团队也并非不可能。沃尔玛就在2011年成立了一支独立的增长团队。它采取的办法是收购硅谷一家知名创业公司 Kosmix 的创新中心,而这个创新中心就成了“沃尔玛实验室”。这个团队作为专注于电子商务的部门独立运作,聚焦沃尔玛网站和 App 的数字创新计划,其中就包括我们在前言里提到的非常成功的“低价捕手”App。团队同时也负责对数字创业公司中的“潜力股”进行收购,比如时尚搜索应用 Stylr 和网络食谱公司 Yumprint,并将这些公司的技术和人才用于提升沃尔玛的电商业务。

    需要强调的一点是,即便增长团队享有充分的独立性,它仍然需要公司高层的大力支持,以应对组织内部的敏感问题和可能出现的摩擦,比如产品、市场、设计和工程人员各自都有自己的行事方式和工作重心,增长团队在和他们打交道时很可能会遇到一些问题。

    如何化解阻力

    一家公司在成立第一支增长团队时难免会遇到一些阻力。除了组织架构和规范尚未完全确立的初创公司外,对于大多数企业来说,建立一个或多个增长团队都意味着重大的人事和汇报制度的调整。不论这样的调整是长期的还是仅限于某一特定的短期增长任务,都需要牵涉一部分人员的工作安排和职责变动。有过在企业工作经历的人都知道,做出这样的调整势必会遇到一些挑战。

    从根本上看,大多数的摩擦都是由企业文化造成的。包括市场、产品开发和软件工程在内的很多部门的人员对于工作安排都有着既成的认知,即他们的团队应当做什么,应当如何去做。在 BitTorrent,市场部最初的想法是只关注用户获取,数据分析则由公司的数据团队来完成,而且分析完全是按照产品团队的要求进行。测试工作并不属于任何部门的工作范围,几乎完全被忽略了。可想而知,当成立增长团队并打破原有的部门筒仓时,公司需要下不少功夫来做出相应调整。

    诸如此类的摩擦在成立增长团队的过程中屡见不鲜。乔西 · 施瓦扎佩尔加入雅虎,成立并领导增长团队时,主要任务是实现公司移动产品的增长。他回忆说,当他的团队开始试验向访客推广雅虎的应用时,遭到了品牌团队的排挤,因为他们认为增长团队的做法偏离了原有信息传达方式所使用的风格和语音提示。产品经理也十分谨慎,因为增长团队所做试验触及的人群和影响力都太广了,每一个在移动设备上访问雅虎网站的人都会看到他们做出的调整。克服这一阻力需要跨团队通力合作,也需要建立团队之间的信任。“我们需要付出大量的努力以赢得伙伴团队的支持。”施瓦扎佩尔回忆说。

    造成摩擦的另一个原因,是增长试验及其所需动用的资源可能会干扰或者牺牲开展既有项目和工作所需要的时间或资源。例如,在 BitTorrent,安娜贝尔花了大量时间设计试验并分析试验结果,这占据了她开展更加迫切的收购工作的时间。而且,随着团队对数据分析的需求不断增加,数据团队变得不堪重负,直到管理层决定为数据团队招兵买马,情况才有所缓解。

    摩擦的产生也是因为增长团队将领域和背景如此不同的一群人聚集在一起,可想而知,他们势必会有意见相左的时候,有时甚至在立场观点和工作重心上存在冲突。工程师一般只对在技术上最具挑战性的工作感兴趣,而不关心他们提出的解决方案是否会对增长产生实质性的影响。产品经理往往沉迷于产品开发与发布工作,如果市场和销售团队在最后一分钟提出产品改动要求却给不出有说服力的商业理由,他们可能会大发雷霆。用户体验设计师经常会反对为产品引入一些试验性的功能或特征,因为他们担心原本对产品满意的用户会因此感到厌烦。营销人员则可能会过于关注像网站访问量或销售线索这样的“面子指标”,而忽略了在漏斗的其他部分(比如用户留存)提升指标的必要性。

    此外,这些立场在上述每个群体的成员中往往都根深蒂固,这不仅是组织结构设计使然,他们相应的入职培训、各自的心态及其背后的激励因素都造成了这样的结果。因此,即便在初创企业,让这些来自不同领域的人携手合作都可能是个不小的挑战。

    如果管理得当,增长团队完全可以减少这些摩擦,前提是公司要为整个团队实现共同的增长目标提供充分的激励和奖励措施。缓解矛盾的另一个方式是确保在决定试验的优先顺序以及评价试验结果时严格用数据说话,而不是仅凭主观臆断。脸谱网增长团队前负责人卡马斯 · 帕里哈比提亚将这样的主观臆断称为公司掌握产品设计、消费者需要方向的“学问”。每个公司,不论大小,都有一些根深蒂固的“学问”,我们应当通过以数据驱动的试验来打破它们。例如,在 Qualaroo,员工一度认为如果产品涨价公司就没法保持成功,这也导致公司的发展止步不前。然而,他们在做了价格试验之后发现,在提价超过400%的情况下公司仍然能够通过吸引一个新的用户群而获得增长。

    当数据分析给出了强有力的结果时,反对意见就很容易处理了。而设计严密的试验带来的结果也会让人无法辩驳,这也能够让人摆脱对自己的某种愿景或策略的执念。而且如果试验过程完全基于数据,团队成员往往会认可这一学习过程的严谨性,并且它可以为成员提供试错的空间,通过不断试错找到最佳做法并取得成功。

    毕竟,成功是化解冲突、消除分歧的最有效手段。很多增长团队的成员都表示,随着他们不断努力取得令人惊叹的成果,不仅团队内部对增长黑客方法的热情高涨,这种热情也不断蔓延至整个公司。

    团队的演进

    随着公司的不断增长和演变,增长团队也应当随之变化。在脸谱网,增长团队从最初的5人迅速壮大成一个庞大的群体,并且聚焦多个领域,比如国际市场和新兴市场手机业务领域。

    通常来说,随着公司的扩张,员工越来越多,增长团队的构成和关注点也会随着时间的推移而有所变化。这时,增长团队可能会从特定部门或者新增部门招募更多人手,可能也会划分成不同小组,分别负责公司不同业务板块下的更加具体的增长计划。Pinterest 的增长团队就从一个独立团队演变成了产品部门下的四个小团队,分别负责用户体验的不同方面。在推特,乔西 · 埃尔曼的团队也从一个一开始只关注“用户引导”的小组演变为一支规模更大、责任更广的增长团队。其他专业人员也可能加入团队提供特定领域的技术或知识支持,他们可能是公司内部的固定员工,也可能是从外部顾问公司或代理公司聘请的临时雇员。公司在扩张的同时应保持并扩大增长团队的规模,以保持以增长为中心的企业文化。即使是最具创意的产品和想法也会因为缺乏持续的改进而止步不前、最终陨灭,这在现实中常常发生,而增长团队是避免这种惨痛结局的法宝。

    对于初创企业来说,增长团队一般都规模很小,需要从团队外部聘请擅长用户增长某一领域(比如用户获取或留存)的专家。对它们来说,为团队增加外部实力能够带来诸多裨益,正如 Dropbox 当初聘请肖恩一样。这样的小团队能将其成员对产品及公司的深刻了解与外部实力结合起来,从而获得丰硕的成果。需要注意的是,不论是在初期阶段还是其他任何时期,都不能将增长的核心职责外包出去。增长工作太重要了,不能轻易交给他人去做,而外部顾问常常缺乏足够的权力、时间或内在动力为实现公司可持续的增长而攻坚克难。

    万事开头难

    增长黑客过程可能看起来很难落实。搭建一个跨职能的团队并不容易,各个部门的经理可能会反对增长团队占用他们手下员工的时间,大量的试验也会让很多人望而却步。总之,势必会有人说“不”,势必会遇到阻力。好在增长黑客能够带来良性增长循环。目标非常具体的小型团队往往会通过推行增长黑客过程取得一系列的成果,而这能够在整个公司激起员工对于这一过程的热情。一旦人们看到这一以数据驱动的试验方法产生的增长创意的威力,势必会萌生对这一过程的热情,而这种热情如星星之火可以燎原。

    不论是在公司还是某个部门内部,增长黑客的推行都不是一蹴而就的。可以试着先成立一个团队使其只专注于某一个产品,甚至是产品的某一个方面,比如网站的注册页面。或者你也可以让团队只负责优化公司的某一个获客渠道,比如加强脸谱网上的获客,或是扩大公司博客的读者群,又或是提高公司邮件营销的效果。增长团队也可以只关注某一个指标,比如提高激活过程中的转化率,或者加强用户留存。随着成功的累积,增长团队的责任范围可以逐渐扩大,或者公司可以成立更多的增长团队。

    如果你是小团队负责人,希望尝试一下增长黑客过程,那么最好能够先获得一些支持,哪怕只是几个同事和一个上司的认可。在推行过程中难免会犯错,试验也会有失败,网页可能会崩溃,这些都是这一试验过程的必经之路。而上级的支持可以大大减轻这些失败带来的冲击。IBM 的 BlueMix DevOps 团队增长负责人劳伦 · 谢福尔就曾经在开展增长黑客试验的初期启动了一项测试,结果导致产品的主页崩溃。但是她的上司非常支持增长黑客行动,所以她和她的团队很快摆脱了这次失利的阴影。

    增长团队也不应太早启动,因为如果产品不受用户的喜爱,那么任何试验都无法激发持久的增长。当然,对于一个不尽人意的产品,很多公司都会想方设法保持一定的用户忠诚度或者获得足够的销量,但是它们终将失败。

    正因如此,企业在明确其产品对于市场是“不可或缺”还是“有没有都无所谓”之前,都不应制订过于雄心勃勃的增长计划,我们在下一章会更详细地讨论这一点。现在你已经知道如何搭建一个增长团队了,接下来让我们进入这一过程的下一个步骤:团队如何利用客户反馈、严密的试验和测试以及深入的数据分析来评估一个产品是否实现了产品—市场匹配。

    第02章:好产品是增长的根本(上)
    第02章:好产品是增长的根本(中)
    第02章:好产品是增长的根本(下)
    第03章 :确定增长杠杆(上)
    第03章:确定增长杠杆(下)
    第04章:快节奏试验 (上)
    第04章:快节奏试验 (中)
    第04章:快节奏试验 (下)
    第05章:获客:优化成本,扩大规模(上)
    第05章:获客:优化成本,扩大规模(中)
    第05章:获客:优化成本,扩大规模(下)
    第06章:激活:让潜在用户真正使用你的产品(上)
    第06章:激活:让潜在用户真正使用你的产品(中)
    第06章:激活:让潜在用户真正使用你的产品(下)
    第07章:留存:唤醒并留住用户 (上)
    第07章:留存:唤醒并留住用户 (中)
    第07章:留存:唤醒并留住用户 (下)
    第08章:变现:提高每位用户带来的收益(上)
    第08章:变现:提高每位用户带来的收益(中)
    第08章:变现:提高每位用户带来的收益(下)
    第09章:良性循环:维持并加速增长 (上)
    第09章:良性循环:维持并加速增长 (下)
    致谢

    阅读全文: http://gitbook.cn/gitchat/geekbook/5a3c787a902f0f2223e25270

    展开全文
  • 《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式...首先声明,这一篇篇幅很的文章。目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较

    《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式没有。张无忌说记住了一半。张三丰又慢吞吞使了一遍,问他记住多少,张无忌说只记得几招了。张三丰最后又示范了一遍,张无忌想了想说,这次全忘光了。张三丰很满意,于是放心让张无忌与八臂神剑去比试。

    首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较真。写的时候花了好几天,几次想放弃,想放弃的原因不是我自己没有弄清楚,而是觉得自己叙事脉络已经紊乱了,感觉无法让读者整明白,怕把读者带到沟里面去,怕自己让人觉得罗嗦废话。但最后,我决定还是坚持下去,因为在反复纠结 –> 不甘 –> 探索 –> 论证 –> 质疑的过程循环中,我完成了对自己的升华,弄明白长久以来的一些困惑。所以,此文最大的目的是给自己作为一些学习记录,如果有幸帮助你解决一些困惑,那么我心宽慰。如果有错的地方,也欢迎指出批评。

    如果你有这样的困扰:
    1. 一个 View 的 parent 一定是 ViewGroup 吗?

    1. Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。

    2. Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。

    3. 在 xml 中设置一个 View 的属性 layout_width 为 wrap_content 或者 match_parent 而不是具体数值 50dp 时,为什么 view 也有正常的尺寸。

    4. 你或多或者知道 Android 测量时的 3 种布局模式:MeasureSpec.EXACTLY、Measure.AT_MOST、Measure.UNSPECIFIED。但你不大能够把握它们。

    5. 你不但对自定义 View 没有问题,对于自定义 ViewGroup 也不在话下,你明白 Android 给出的 3 种测量模式的含义,但是你还是没有来得及去思考,3 种测量模式本身是什么。

    6. 你也许没有想过 Activity 最外层的 View 是什么。

    7. 你也许知道 Activity 最外层的 View 叫做 DecorView。明白它与 PhoneWindow 及 Activity.setContentView() 的联系。但你不知道谁对 DecorView 进行了尺寸测量。

    好了,文章正式开始。请深吸一口气,take it easy!

    无法忽视的自定义 View 话题

    Android 应用层开发绕不开自定义 View 这个话题,在 Android 中官方称之为 Widget,所以本文中的 View 其实与 Widget也就一个意思。虽然现在 Github 上有形形色色的开源库供大家使用,但是作为一名有想法的开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,如果现有的 Widget 无法完成任务,那么我们就应该想到要自定义一个 View 了。
    我们或多或少知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
    - 测量 onMeasure()
    - 布局 onLayout()
    - 绘制 onDraw()

    没有办法说这三个阶段,那个阶段最重要,只是相对而言,测量阶段对于大多开发者而言难度相对其它两个要大,处理的细节也要多得多,自定义一个 View,正确的测量是第一步,正因为如此今天本文的主题就是讨论 View 中的测量机制和细节。

    测量 View 就是测量一个矩形

    得益于人们的想象力,Android 系统平台上出现了各种各样的 View。有 Button、TextView、ListView 等系统自带的组件,也有更多开发者自定义的 View。
    这里写图片描述

    上面是 Android 系统自带的 Widget 表现,它们用来完成不同功能的交互与效果展示,但对于开发者而言,上面的界面还有这样的一面。
    这里写图片描述

    透过另一个视角来观察,所有的 Widget

    世界万物都有某些运行的规则,或者是突破不了的樊篱。对于一个 View 而言,它本质上就是一个矩形,一块四方的区域,铺开一张画布,然后利用所有的资源,在现有的规则之下天马行空。
    这里写图片描述

    因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!

    既然是矩形,那么它肯定有明确的宽高和位置坐标。宽高是在测量阶段得出,然后在布局阶段,根据实际需要确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,它是画家,这个我们很放心。

    打个比方,政府做城市规划时,房地产商们告诉政府他们希望的用地面积,政府综合政策和用地面积的实际情况,给地产商划分土地面积,地图上就是一个个圈圈。地产商们拿到明确的地域范围信息后,在规定好的区域建造自己的高楼或者大厦。而自定义 View 就是拿到这个类似政府规划的区域范围参数,只不过现实世界中,政府规划给地产商的土地不一定是四四方方的矩形,但是在 Android 中 View 拿到的区域一定是矩形。
    这里写图片描述

    好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。

    View 设置尺寸的基本方法

    接下来的过程,我将会用一系列比较细致的实验来说明问题,觉得罗嗦无聊的同学可以直接跳过这一小节。
    我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。

    <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="test"/>

    这样屏幕上就出现了一个按钮。
    这里写图片描述
    我们再把宽高固定。

    <Button
            android:layout_width="200dp"
            android:layout_height="50dp"
            android:text="test"/>

    这里写图片描述
    再换一种情况,将按钮的宽度由父容器决定

    <Button
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="test"/>
    

    这里写图片描述

    上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。

    1. match_parent 代表这个维度上的值与父窗口一样
    2. wrap_content 表示这个维度上的值由 View 本身的内容所决定
    3. 具体数值如 5dp 表示这个维度上 View 给出了精确的值。

    实验1

    我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>
    

    这里写图片描述
    可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。

    实验2

    <RelativeLayout
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>

    这里写图片描述

    实验3

    <RelativeLayout
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>

    这里写图片描述

    实验4

    <RelativeLayout
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="1000dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>

    这里写图片描述

    似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园问光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。

    Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。

    山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。

    那么,它们的协议是什么?

    View 和 ViewGroup 之间的测量协议 MeasureSpec

    我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。

    public class TestView extends View {
        public TestView(Context context) {
            super(context);
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
    

    onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。

    它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。

    MeasureSpec

    MeasureSpec 是 View.java 中一个静态类

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
    
        public static final int EXACTLY     = 1 << MODE_SHIFT;
    
    
        public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
    
      ......
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
    
    
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    
        ......
    }
    

    MeasureSpec 的代码并不是很多,它有最重要的三个静态常量和三个最重要的静态方法。

    MeasureSpec.UNSPECIFIED
    MeasureSpec.EXACTLY
    MeasureSpec.AT_MOST
    
    MeasureSpec.makeMeasureSpec()
    MeasureSpec.getMode()
    MeasureSpec.getSize()

    MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。

    这里写图片描述

    通过 makeMeasureSpec() 方法将 Mode 和 Size 组合成一个 measureSpec 数值。
    而通过 getMode() 和 getSize() 却可以逆向地将一个 measureSpec 数值解析出它的 Mode 和 Size。

    下面讲解 MeasureSpec 的 3 种测量模式。

    MeasureSpec.UNSPECIFIED

    此种模式表示无限制,子元素告诉父容器它希望它的宽高想要多大就要多大,你不要限制我。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。

    MeasureSpec.EXACTLY

    此模式说明可以给子元素一个精确的数值。

    
    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="test"/>
    

    当 layout_width 或者 layout_height 的取值为 match_parent 或者 明确的数值如 100dp 时,表明这个维度上的测量模式就是 MeasureSpec.EXACTLY。为什么 match_parent 也有精确的值呢?我们可以合理推断一下,子 View 希望和 父 ViewGroup 一样的宽或者高,对于一个 ViewGroup 而言它显然是可以决定自己的宽高的,所以当它的子 View 提出 match_parent 的要求时,它就可以将自己的宽高值设置下去。

    MeasureSpec.AT_MOST

    此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是你不能超过我能提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。
    当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。

    了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。

    自定义 View

    我的目标是定义一个文本框,中间显示黑色文字,背景色为红色。
    这里写图片描述

    我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
    attrs.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="TestView">
            <attr name="android:text" />
            <attr name="android:textSize" />
        </declare-styleable>
    </resources>

    TestView.java

    public class TestView extends View {
    
        private  int mTextSize;
        TextPaint mPaint;
        private String mText;
    
        public TestView(Context context) {
            this(context,null);
        }
    
        public TestView(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
            mText = ta.getString(R.styleable.TestView_android_text);
            mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
    
            ta.recycle();
    
            mPaint = new TextPaint();
            mPaint.setColor(Color.BLACK);
            mPaint.setTextSize(mTextSize);
            mPaint.setTextAlign(Paint.Align.CENTER);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
            int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
    
            canvas.drawColor(Color.RED);
            if (TextUtils.isEmpty(mText)) {
                return;
            }
            canvas.drawText(mText,cx,cy,mPaint);
    
        }
    }
    

    布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.frank.measuredemo.MainActivity">
    
        <com.frank.measuredemo.TestView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:text="test"/>
    
    </RelativeLayout>
    

    效果
    这里写图片描述

    我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。

    <com.frank.measuredemo.TestView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="test"/>
    

    将 layout_width 的值改为 match_parent,所以它的宽是由父类决定,但同样它也正常。
    这里写图片描述

    我们已经知道,上面的两种情况其实就是对应 MeasureSpec.EXACTLY 这种测量模式,在这种模式下 TestView 本身不需要进行处理。

    那么有人会问,如果 layout_width 或者 layout_height 的值为 wrap_content 的话,那么会怎么样呢?

    我们继续测试观察。

    <com.frank.measuredemo.TestView
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="test"/>
    

    这里写图片描述
    效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。

    <com.frank.measuredemo.TestView
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    

    这里写图片描述

    宽度正常,高度却和 ViewGroup 一样了。
    再看一种情况

    <com.frank.measuredemo.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>
    

    这里写图片描述

    这次可以看到,宽高都和 ViewGroup 一致了。

    但是,这不是我想要的啊!

    wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。

    TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。
    我们的思路可以如下:
    1. 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
    2. 对于 MeasureSpec.AT_MOST 模式,我们要根据自己的内容计算宽高,但是数值不得超过 ViewGroup 给出的建议值。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
            int resultW = widthSize;
            int resultH = heightSize;
    
            int contentW = 0;
            int contentH = 0;
    
            /**重点处理 AT_MOST 模式,TestView 自主决定数值大小,但不能超过 ViewGroup 给出的
             * 建议数值
             * */
            if ( widthMode == MeasureSpec.AT_MOST ) {
    
                if (!TextUtils.isEmpty(mText)){
                    contentW = (int) mPaint.measureText(mText);
                    contentW += getPaddingLeft() + getPaddingRight();
                    resultW = contentW < widthSize ? contentW : widthSize;
                }
    
            }
    
            if ( heightMode == MeasureSpec.AT_MOST ) {
                if (!TextUtils.isEmpty(mText)){
                    contentH = mTextSize;
                    contentH += getPaddingTop() + getPaddingBottom();
                    resultH = contentH < widthSize ? contentH : heightSize;
                }
            }
    
            //一定要设置这个函数,不然会报错
            setMeasuredDimension(resultW,resultH);
    
    }
    
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
    
        metrics = mPaint.getFontMetrics();
        cy += metrics.descent;
    
        canvas.drawColor(Color.RED);
        if (TextUtils.isEmpty(mText)) {
            return;
        }
        canvas.drawText(mText,cx,cy,mPaint);
    
    }
    

    代码并不难,我们可以做验证。

    <com.frank.measuredemo.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:text="test"/>
    

    这里写图片描述
    可以看到这才是我们想要的效果。它现在完成了对 MeasureSpec.AT_MOST 模式的适配。我们再验证一下另外一种情况

    <com.frank.measuredemo.TestView
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="48sp"
        android:text="test"/>
    
    

    这里写图片描述
    在 MeasureSpec.EXACTLY 模式下同样没有问题。

    现在,我们已经掌握了自定义 View 的测量方法,其实也很简单的嘛。

    但是,还没有完。我们验证的刚刚是自定义 View,对于 ViewGroup 的情况是有些许不同的。

    View 和 ViewGroup,鸡生蛋,蛋生鸡的关系

    ViewGroup 是 View 的子类,但是 ViewGroup 的使命却是装载和组织 View。这好比是母鸡是鸡,母鸡下蛋是为了孵化小鸡,小鸡长大后如果是母鸡又下蛋,那么到底是蛋生鸡还是鸡生蛋?

    这里写图片描述

    自定义 View 的测量,我们已经掌握了,那现在我们编码来测试自定义 ViewGroup 时的测量变现。
    假设我们要制定一个 ViewGroup,我们就给它起一个名字叫 TestViewGroup 好了,它里面的子元素按照对角线铺设,如下图:
    这里写图片描述

    前面说过 ViewGroup 本质上也是一个 View,只不过它多了布局子元素的义务。既然是 View 的话,那么自定义一个 ViewGroup 也需要从测量开始,问题的关键是如何准确地得到这个 ViewGroup 尺寸信息?

    我们还是需要仔细讨论。

    1. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.EXACTLY 时,这时候的尺寸就可以按照父容器传递过来的建议尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一个 View。
    2. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,这就需要 TestViewGroup 自己计算这个维度上的尺寸数值。就上面给出的信息而言,TestViewGroup 的尺寸非常简单,那就是在一个维度上用自身 padding + 各个子元素的尺寸(包含子元素的宽高+子元素设置的 marging )得到一个可能的尺寸数值。然后用这个尺寸数值与 TestViewGroup 的父容器给出的建议 Size 进行比较,最终结果取最较小值。
    3. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,因为要计算子元素的尺寸,所以如何准确得到子元素的尺寸也是至关重要的事情。好在 Android 提供了现成的 API。
    4. 当 TestViewGroup 测量成功后,就需要布局了。自定义 View 基本上不要处理这一块,但是自定义 ViewGroup,这一部分却不可缺少。但本篇文章不是讨论布局技巧的,只是告诉大家布局其实相对而言更简单一点,无非是确定好子元素的坐标然后进行布局。

    接下来,我们就可以具体编码了。

    public class TestViewGroup extends ViewGroup {
    
    
        public TestViewGroup(Context context) {
            this(context,null);
        }
    
        public TestViewGroup(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
        }
    
    
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            //只关心子元素的 margin 信息,所以这里用 MarginLayoutParams
            return new MarginLayoutParams(getContext(),attrs);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
            int resultW = widthSize;
            int resultH = heightSize;
    
            /**计算尺寸的时候要将自身的 padding 考虑进去*/
            int contentW = getPaddingLeft() + getPaddingRight();
            int contentH = getPaddingTop() + getPaddingBottom();
    
            /**对子元素进行尺寸的测量,这一步必不可少*/
            measureChildren(widthMeasureSpec,heightMeasureSpec);
    
            MarginLayoutParams layoutParams = null;
    
            for ( int i = 0;i < getChildCount();i++ ) {
                View child = getChildAt(i);
                layoutParams = (MarginLayoutParams) child.getLayoutParams();
    
                //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
                if ( child.getVisibility() == View.GONE ) {
                    continue;
                }
    
                contentW += child.getMeasuredWidth()
                        + layoutParams.leftMargin + layoutParams.rightMargin;
    
                contentH += child.getMeasuredHeight()
                        + layoutParams.topMargin + layoutParams.bottomMargin;
            }
    
            /**重点处理 AT_MOST 模式,TestViewGroup 通过子元素的尺寸自主决定数值大小,但不能超过
             *  ViewGroup 给出的建议数值
             * */
            if ( widthMode == MeasureSpec.AT_MOST ) {
                resultW = contentW < widthSize ? contentW : widthSize;
            }
    
            if ( heightMode == MeasureSpec.AT_MOST ) {
                resultH = contentH < heightSize ? contentH : heightSize;
            }
    
            //一定要设置这个函数,不然会报错
            setMeasuredDimension(resultW,resultH);
    
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
            int topStart = getPaddingTop();
            int leftStart = getPaddingLeft();
            int childW = 0;
            int childH = 0;
            MarginLayoutParams layoutParams = null;
            for ( int i = 0;i < getChildCount();i++ ) {
                View child = getChildAt(i);
                layoutParams = (MarginLayoutParams) child.getLayoutParams();
    
                //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
                if ( child.getVisibility() == View.GONE ) {
                    continue;
                }
    
                childW = child.getMeasuredWidth();
                childH = child.getMeasuredHeight();
    
                leftStart += layoutParams.leftMargin;
                topStart += layoutParams.topMargin;
    
    
                child.layout(leftStart,topStart, leftStart + childW, topStart + childH);
    
                leftStart += childW + layoutParams.rightMargin;
                topStart += childH + layoutParams.bottomMargin;
            }
    
        }
    
    }

    然后我们将之添加进 xml 布局文件中进行测试。

    <com.frank.measuredemo.TestViewGroup
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        <com.frank.measuredemo.TestView
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:text="test"/>
        <TextView
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:background="#00ff40"
            android:text="test"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            android:text="test"/>
    </com.frank.measuredemo.TestViewGroup>

    那么实际效果如何呢?
    这里写图片描述

    再试验一下给 TestViewGroup 加上固定宽高。

    <com.frank.measuredemo.TestViewGroup
        android:layout_width="350dp"
        android:layout_height="600dp"
        android:background="#c3c3c3">
        <com.frank.measuredemo.TestView
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:text="test"/>
        <TextView
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:background="#00ff40"
            android:text="test"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            android:text="test"/>
    </com.frank.measuredemo.TestViewGroup>
    

    结果如下:
    这里写图片描述

    自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。

    但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。

    TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义套路。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 ViewGroup。

    我个人觉得,尝试自己动手去实现一个流式标签控件,对于提高自定义 ViewGroup 的能力是有很大的提高,因为只有在自己实践的时候,你都会思考,在思考和实验的过程你才会深刻的理解测量机制的用途。

    不过自定义一个流式标签控件是另外一个话题了,也许我会另外开一篇来讲解,不过我希望大家亲自动手去实现它。下面是我自定义 ViewGroup 的一个实例截图。
    这里写图片描述


    洋洋洒洒写了这么多的内容,其实基本上已经完结了,已经不耐烦的同学可以直接跳转到后面的总结。但是,对于有钻研精神的同学来讲,其实还不够。还没有完。

    问题1:到底是谁在测量 View ?

    问题2:到底是什么时候需要测量 View ?

    针对问题 1:
    我们在自定义 TestViewGroup 的时候,在 onMeasure() 方法中,通过了一个 API 对子元素进行了测量,这个 API 就是 measureChildren()。这个方法进行了什么样的处理呢?我们可以去看看。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
    
    protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
    
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    代码简短易懂,分别调用 child 的 measure() 方法。值得注意的是,传递给 child 的测量规格已经发生了变化,比如 widthMeasureSpec 变成了 childWidthMeasureSpec。原因是这两行代码:

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    

    硬着头皮再去看 getChildMeasureSpec() 方法的实现。

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            // 父类本身就是 EXACTLY 模式下
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    // 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    // child 希望尺寸和 parent 一样大,那么满足它
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    // child 希望由自己决定自己的尺寸,但是有个前提是它不能大于 parent 本身给的建议值。
                    // 并且需要设置它的测量模式为 AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            // 父类本身就是 AT_MOST 模式下,对于 parent 本身而言,它也有个最大的尺寸约束
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    // 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    // child 希望尺寸和 parent 一样大,那么满足它,但是 parent 本身有限制,所以也
                    // 需要给 child 加一个限制,这个限制就是将 child 模式设置为 AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    // child 希望由自己决定自己的尺寸,但是有个前提是它不能大于 parent 本身给的建议值。
                    // 并且需要设置它的测量模式为 AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            // 父类本身就是 UNSPECIFIED 模式下,对于 parent 本身而言,它的期望值是想要多大就多大,让 parent 的 parent不要限制它。
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    //如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    // child 希望尺寸和 parent 一样大,那么满足它,但是 parent 本身也需要计算,所以只能设置为0,并且
                    // 将child的测量模式设置为 UNSPECIFIED
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    // child 希望尺寸由自己决定,一般这个时候,parent会给它一个 Size 作为最大值限制,
                    // 但是 parent 本身也需要计算,所以只能设置为0,并且没有给child最大值的设定
                    // 将child的测量模式设置为 UNSPECIFIED
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    
    

    代码很清楚,很多事情真相大白了。解释了很多东西,包括:

    1. 为什么 layout_width 或者 layout_height 设置了精确的数值时,它的测量模式为 MeasureSpec.EXACTLY。

    2. 为什么 layout_width 或者 layout_height 设置为 match_parent 时,不一定保证它的测量模式为 MeasureSpec.EXACTLY。它也有可能是 MeasureSpec.AT_MOST,也有可能是 MeasureSpec.UNSPECIFIED。

    3. 为什么 layout_width 或者 layout_height 设置为 wrap_content 时,它最可能的测量模式为 MeasureSpec.AT_MOST,但也有可能是 MeasureSpec.UNSPECIFIED。

    我们继续向前,ViewGroup 的 measureChild() 方法最终会调用 View.measure() 方法。我们进一步跟踪。

    
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    
            // Optimize layout by avoiding an extra EXACTLY pass when the view is
            // already measured as the correct size. In API 23 and below, this
            // extra pass is required to make LinearLayout re-distribute weight.
            final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                    || heightMeasureSpec != mOldHeightMeasureSpec;
            final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                    && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
            final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                    && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
            final boolean needsLayout = specChanged
                    && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    
            if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
    
    
                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
    
    
                    // measure ourselves, this should set the measured dimension flag back
                    // onMeasure 在此调用
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } 
    
                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }
    
                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
    
        }
    

    代码比较长,删掉了一些与主题无关的代码,我们可以看到,当一个 view 的 measure() 方法调用时,如果它此次的 measureSpec 与 旧的不相同或者是它需要重新布局也就是 requestLayout() 方法被调用时,它会触发 onMeasure() 方法。值得注意的是最后它会检查变量 mPrivateFlags 的一个位 PFLAG_MEASURED_DIMENSION_SET,如果这一位没有置为 1 的话,会抛出异常。而这一位在 onMeasure() 方法中一定要调用。

    好了,鸡生蛋,蛋生鸡结论似乎有个结果了。是 ViewGroup 中的 onMeasure() 调用了 View.measure() 而 View.measure() 调用了 View.onMeasure()。

    这里写图片描述

    于是,我们终于明白了。

    但是呢,还能更深入一点吗?

    Activity 中的道,最顶层的那个 View?

    道生一,一生二,二生三,三生万物,万物负阴而抱阳,冲气以为和。– 《道德经》

    我们已经知道,不管是对于 View 还是 ViewGroup 而言,测量的起始是 measure() 方法,沿着控件树一路遍历下去。那么,对于 Android 一个 Activity 而言,它的顶级 View 或者顶级 ViewGroup 是哪一个呢?

    从 setContentView 说起

    我们知道给 Activity 布局的时候,在 onCreate() 中设置 setContentView() 的资源文件就是我们普通开发者所能想到的比较顶层的 View 了。比如在 activity_main.xml 中设置一个 RelativeLayout,那么这个 RelativeLayout 就是 Activity 最顶层的 View 吗?谁调用它的 measure() 方法触发整个控件树的测量?

    
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initActionBar();
    }
    
    public Window getWindow() {
        return mWindow;
    }
    
    /**
     * Abstract base class for a top-level window look and behavior policy.  An
     * instance of this class should be used as the top-level view added to the
     * window manager. It provides standard UI policies such as a background, title
     * area, default key processing, etc.
     *
     * <p>The only existing implementation of this abstract class is
     * android.policy.PhoneWindow, which you should instantiate when needing a
     * Window.  Eventually that class will be refactored and a factory method
     * added for creating Window instances without knowing about a particular
     * implementation.
     */
    public abstract class Window {
    
    }
    
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
    
    }
    

    可以看到,调用 Activity.setContentView() 其实就是调用 PhoneWindow.setContentView()。

    PhoneWindow.java

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    

    注意,在上面代码中显示,通过 setContentView 传递进来的 view 被添加到了一个 mContentParent 变量上了,所以可以回答上面的问题,通过 setContentView() 中传递的 View 并不是 Activity 最顶层的 View。我们再来看看 mContentParent。

    它只是一个 ViewGroup。我们再把焦点聚集到 installDecor() 这个函数上面。

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
    
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
    
            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();
    
            mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
            if (mTitleView != null) {
                mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
                if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                    View titleContainer = findViewById(com.android.internal.R.id.title_container);
                    if (titleContainer != null) {
                        titleContainer.setVisibility(View.GONE);
                    } else {
                        mTitleView.setVisibility(View.GONE);
                    }
                    if (mContentParent instanceof FrameLayout) {
                        ((FrameLayout)mContentParent).setForeground(null);
                    }
                } else {
                    mTitleView.setText(mTitle);
                }
            } else {
                mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
    
    
            }
        }
    }
    

    代码很长,我删除了一些与主题无关的代码。这个方法体内引出了一个 mDecor 变量,它通过 generateDecor() 方法创建。DecorView 是 PhoneWindow 定义的一个内部类,实际上是一个 FrameLayout。

    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    
    }
    

    我们回到 generate() 方法

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }
    

    DecorView 怎么创建的我们已经知晓,现在看看 mContentParent 创建方法 generateLayout()。它传递进了一个 DecorView,所以它与 mDecorView 肯定有某种关系。

    protected ViewGroup generateLayout(DecorView decor) {
            // Apply data from current theme.
    
    
    
    
        WindowManager.LayoutParams params = getAttributes();
    
    
    
        // Inflate the window decor.
    
    
    
        // Embedded, so no decoration is needed.
        layoutResource = com.android.internal.R.layout.screen_simple;
        // System.out.println("Simple!");
    
    
        mDecor.startChanging();
    
        View in = mLayoutInflater.inflate(layoutResource, null);
    
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
    
    
        mDecor.finishChanging();
    
        return contentParent;
    }
    
    

    原代码很长,我删除了一些繁琐的代码,整个流程变得很清晰,这个方法内 inflate 了一个 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是这个被添加进去的 view 中。
    这个 xml 文件是 com.android.internal.R.layout.screen_simple,我们可以从 SDK 包中找出它来。
    这里写图片描述

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    就是一个 LinearLayout ,方向垂直。2 个元素,一个是 actionbar,一个是 content。并且 ViewStub 导致 actionbar 需要的时候才会进行加载。

    总之由以上信息,我们可以得到 Activity 有一个 PhoneWindow 对象,PhoneWindow 中有一个 DecorView,DecorView 内部有一个 LinearLayout,LinearLayout 中存在 id 为 android:id/content 的布局 mContentParent。mContentParent 用来加载 Activity 通过 setContentView 传递进来的 View。所以整个结构呼之欲出。

    注意:因为代码有删简,实际上 LinearLayout 由两部分组成,下面的是 Content 无疑,上面的部分不一定是 ActionBar,也可能是 title,不过这不影响我们,我们只需要记住 content 就好了。

    这里写图片描述

    DecorView 才是 Activity 中整个控件树的根。

    谁测绘了顶级 View ?

    既然 DecorView 是整个测绘的发起点,那么谁对它进行了测绘?谁调用了它的 measure() 方法,从而导致整个控件树自上至下的尺寸测量?

    我们平常开发知道调用一个 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?

    我们再回到 PhoneWindow 的 setContentView() 中来。

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    

    我们看看 mContentParent.addView(view, params) 的时候发生了什么。

    public void addView(View child, int index, LayoutParams params) {
        if (DBG) {
            System.out.println(this + " addView");
        }
    
        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }
    

    它调用了 requestLayout(),正好要探索它,所以说 setContentView 之后,控件树会进行一次测绘。不过这里是结论,其过程需要我们来验证。

    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
    
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
    
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
    
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
    

    View 在 requestLayout() 中给 mPrivateFlags 添加了 2 个标记 PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED。还引出了另外一个对象 ViewRootImpl。然后调用 parent 的 requestLayout()。

    我们在想最顶层的 View 是 DecorView,但是它有 parent 吗?如果有的话,那是什么?我在此困扰了好久,觉得 DecorView 没有父容器,所以我把自己追踪的线索弄丢了,好在后来找到了。现在可以提示大家一下。
    View 中的 mParent 其实是一个接口:ViewParent。

    /**
     * Defines the responsibilities for a class that will be a parent of a View.
     * This is the API that a view sees when it wants to interact with its parent.
     * 
     */
    public interface ViewParent {
        /**
         * Called when something has changed which has invalidated the layout of a
         * child of this view parent. This will schedule a layout pass of the view
         * tree.
         */
        public void requestLayout();
    
       ......
    
    }
    

    我之前犯糊涂了,以为一个 View 的 parent 一定是 ViewGroup。其实 ViewGroup 只是 ViewParent 接口的实现类之一。

    public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    }
    

    我们再看看 ViewRootImpl 的源码。

    
    /**
     * The top of a view hierarchy, implementing the needed protocol between View
     * and the WindowManager.  This is for the most part an internal implementation
     * detail of {@link WindowManagerGlobal}.
     *
     * {@hide}
     */
    @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
    public final class ViewRootImpl implements ViewParent,
            View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    
    }

    可以看到 ViewRootImpl 也是 ViewParent 的实现类,所以说理论上它也可以成为 DecorView 的 parent。我们再注意到注释解释说 ViewRootImpl 是控件树的最顶端。更多的细节要我去查找 WindowManagerGlobal。好吧,那就找吧。

    
    /**
     * Provides low-level communication with the system window manager for
     * operations that are not associated with any particular context.
     *
     * This class is only used internally to implement global functions where
     * the caller already knows the display and relevant compatibility information
     * for the operation.  For most purposes, you should use {@link WindowManager} instead
     * since it is bound to a context.
     *
     * @see WindowManagerImpl
     * @hide
     */
    public final class WindowManagerGlobal {
    
    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            if (view == null) {
                throw new IllegalArgumentException("view must not be null");
            }
            if (display == null) {
                throw new IllegalArgumentException("display must not be null");
            }
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
            }
    
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
            if (parentWindow != null) {
                parentWindow.adjustLayoutParamsForSubWindow(wparams);
            }
    
            ViewRootImpl root;
            View panelParentView = null;
    
            synchronized (mLock) {
    
    
                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
    
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                synchronized (mLock) {
                    final int index = findViewLocked(view, false);
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                }
                throw e;
            }
        }
    
    
    }

    代码不是很多,其中 addView() 方法引起了我的兴趣。
    在此,创建了 ViewRootImpl 对象。并且这个类的注释叫我去查看 WindowManagerImpl。

    感觉越陷越深了,没有办法,深吸一口气,继续向前。

    public final class WindowManagerImpl implements WindowManager {
        private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
        private final Display mDisplay;
        private final Window mParentWindow;
    
        public WindowManagerImpl(Display display) {
            this(display, null);
        }
    
    
        @Override
        public void addView(View view, ViewGroup.LayoutParams params) {
            mGlobal.addView(view, params, mDisplay, mParentWindow);
        }
    
    
    }
    

    原来 WindowManagerImpl 的内部有一个 WindowManagerGlobal 对象,WindowManagerGlobal 代理 WindowManagerImpl ,并且 WindowManagerImpl 是 WindowManager 的实现类。
    也就是我们平常编写悬浮窗经常用到的。

    WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    

    已经走到这一步了,貌似线索已经断了,不过,需要明白一点是 Activity 也是一个 Window,所以创建 Activity 的时候一定会通过 WindowManager addView。

    把焦点回到 Activity,里面还有个 PhoneWindow 值得我们挖掘。在 Activity 源码中搜索 window 被赋值的地方。结果找到了。

    final void attach(Context context, ActivityThread aThread,
                Instrumentation instr, IBinder token, int ident,
                Application application, Intent intent, ActivityInfo info,
                CharSequence title, Activity parent, String id,
                NonConfigurationInstances lastNonConfigurationInstances,
                Configuration config) {
            attachBaseContext(context);
    
            mFragments.attachActivity(this, mContainer, null);
    
            mWindow = PolicyManager.makeNewWindow(this);
            mWindow.setCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);
    
            mUiThread = Thread.currentThread();
    
            mMainThread = aThread;
            mInstrumentation = instr;
            mToken = token;
            mIdent = ident;
            mApplication = application;
            mIntent = intent;
            mComponent = intent.getComponent();
            mActivityInfo = info;
            mTitle = title;
            mParent = parent;
            mEmbeddedID = id;
            mLastNonConfigurationInstances = lastNonConfigurationInstances;
    
            mWindow.setWindowManager(
                    (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                    mToken, mComponent.flattenToString(),
                    (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            if (mParent != null) {
                mWindow.setContainer(mParent.getWindow());
            }
            mWindowManager = mWindow.getWindowManager();
            mCurrentConfig = config;
     }
    

    mWindow 通过 PolicyManager 创建。并且与 WindowManager建立了联系。

    public final class PolicyManager {
        private static final String POLICY_IMPL_CLASS_NAME =
            "com.android.internal.policy.impl.Policy";
    
        private static final IPolicy sPolicy;
    
        static {
            // Pull in the actual implementation of the policy at run-time
            try {
                Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
                sPolicy = (IPolicy)policyClass.newInstance();
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
            } catch (InstantiationException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
            }
        }
    
        // Cannot instantiate this class
        private PolicyManager() {}
    
        // The static methods to spawn new policy-specific objects
        public static Window makeNewWindow(Context context) {
            return sPolicy.makeNewWindow(context);
        }
    
    }
    

    通过反射创建了一个 Policy 对象,然后调用它的 makeNewWindow() 方法得到了一个 window() 对象。

    public class Policy implements IPolicy {
        private static final String TAG = "PhonePolicy";
    
        private static final String[] preload_classes = {
            "com.android.internal.policy.impl.PhoneLayoutInflater",
            "com.android.internal.policy.impl.PhoneWindow",
            "com.android.internal.policy.impl.PhoneWindow$1",
            "com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback",
            "com.android.internal.policy.impl.PhoneWindow$DecorView",
            "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
            "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
        };
    
        static {
            // For performance reasons, preload some policy specific classes when
            // the policy gets loaded.
            for (String s : preload_classes) {
                try {
                    Class.forName(s);
                } catch (ClassNotFoundException ex) {
                    Log.e(TAG, "Could not preload class for phone policy: " + s);
                }
            }
        }
    
        public Window makeNewWindow(Context context) {
            return new PhoneWindow(context);
        }
    
    
    }
    

    到此,我们终于知道了 Activity 中的 mWindow 就是一个 PhoneWindow。

    兜兜转转,我们似乎回到了原点,不过值得庆幸的是我们知道了 Activity 中 PhoneWindow 的由来。

    顶层的 View,由谁测绘?

    为了避免大家遗忘,我把问题又抛出来。这是我们的终极目标,明确了这个答案之后,本篇文章就结束了。

    不过,对于现在而言,目前掌握的信息,我们似乎无从得知,依照上面的思路步骤,我们无法找到这个问题的答案。此路不通,我们就需要想想其它办法了。我们要寻找的是,最开始的时候是谁调用了 DecorView.measure() 方法最终导致整个控件树的重绘。现在线索继了,我们只得再从 Activity 身上挖掘。

    说说 Activity 相关

    我之前阅读过 ActivityManager、WindowManager、PackageManager 的源码。我们大多最关心的是 Activity 的生命周期流程。

    是的,我们在 Activity 中的 onCreate() 方法中加载布局文件,但是却要在 onResume() 之后,图像才能显示出来。所以,我们可以从这里入手。

    Activity 的流程我只简单过一篇,因为是另外的主题了,并且这个主题很庞大繁琐,所以很多的细节我会选择忽略。不熟悉的同学可以自己翻阅资料。

    Activity 的创建

    Java 学习的时候,大家都知道要运行起来就要写一个类,然后包含一个 main() 方法,但是 Activity 没有让我们实现 main(),程序也照样跑起来了,这个估计是很多 Android 初学者比较困扰的地方。其实,Activity 是运行在 Android Framework 之上的,我们编写的程序其实是运行在 Google 工程师们弄好的程序之上,可以说是别人程序中的片断,这就是框架。Android 的应用运行的时候会创建一个包含 main() 方法的类,这个类叫 ActivityThread。一看名字就知道与 Activity 有关。
    我们再来引出另一个类 ActivityManagerService,它是系统最重要的服务之一,用来调度和处理应用层客户端传递过来的相关的请求。

    Activity 的 onResume() 由来

    Activity 第一次启动时,肯定是先执行 onCreate() 然后才执行 onResume() 方法的。

     private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    
    
           ·
        Activity a = performLaunchActivity(r, customIntent);
    
        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            Bundle oldState = r.state;
    
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);
    
        } else {
            // If there was an error, for any reason, tell the activity
            // manager to stop us.
            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(r.token, Activity.RESULT_CANCELED, null);
            } catch (RemoteException ex) {
                // Ignore
            }
        }
    }

    可以看到是先执行 performLaunchActivity() 再执行 handleResumeActivity() 的。performLauncherActivity() 中是创建 Activity 并调用 onCreate() 方法。感兴趣的同学自己去查阅代码。

    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
                boolean reallyResume) {
    
    
        ActivityClientRecord r = performResumeActivity(token, clearHide);
    
        if (r != null) {
            final Activity a = r.activity;
    
    
            if (r.window == null && !a.mFinished && willBeVisible) {
                // 重点在这一段代码
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
    
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
    
                    wm.addView(decor, l);
    
                }
            }
        }   
    }
    

    可能看到 ActivityThread 在 handleResumeActivity() 方法中调用了 WindowManager.addView() 方法,这个方法最终调用的就是本文前面部分已经讲到过的 WindowManagerGlobal.addView()。

    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
    
    
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    
        ViewRootImpl root;
        View panelParentView = null;
    
        synchronized (mLock) {
    
            root = new ViewRootImpl(view.getContext(), display);
    
            view.setLayoutParams(wparams);
    
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
    
        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }
    

    之前我们看到这一段代码的时候,不明白它的用途。现在可以算是明白了。Activity 创建后,onResume() 执行的时候,要将 DecorView 保存进 ViewRootImpl。

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                if (mView == null) {
                    mView = view;
    
                    mAdded = true;
    
                    // Schedule the first layout -before- adding to the window
                    // manager, to make sure we do the relayout before receiving
                    // any other events from the system.
                    requestLayout();
    
    
                    view.assignParent(this);
    
                }
            }
    
    }
    

    我精简了代码,注意 ViewRootImpl 将自己设置成为了 DecorView 的 parent,这说明了一个问题:

    控件树中任何一个 View 调用 requestLayout() 方法,最终会调用 Activity 相关联的 ViewRootImpl 的 requestLayout() 方法,可以说 ViewRootImpl 操纵了整棵控件树,它的名字名符其实

    通过 setView() 方法 ViewRootImpl 将 DecorView 保存到 mView 变量中,并执行了 ViewRootImpl.requestLayout() 方法。

    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            scheduleConsumeBatchedInput();
        }
    }

    mChoreographer 是一个 Choreographer 对象,你可以简单把它当作一个 Handler 就好,用来编排 Android 界面的绘制。它最终会调用 doTraversal() 方法。

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
    
            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
    
        }
    }
    

    最终调用的是ViewRootImpl 的 performTraversals() 方法。这个代码非常非常非常的长。我只节选我们需要的部分,完整信息请参照源码。

    private void performTraversals() {
            // cache mView since it is used so much below...
            final View host = mView;
    
    
            if (host == null || !mAdded)
                return;
    
            mIsInTraversal = true;
    
    
            WindowManager.LayoutParams lp = mWindowAttributes;
    
            int desiredWindowWidth;
            int desiredWindowHeight;
    
    
    
            WindowManager.LayoutParams params = null;
            if (mWindowAttributesChanged) {
                mWindowAttributesChanged = false;
                surfaceChanged = true;
                params = lp;
            }
    
    
    
            if (mFirst) {
    
    
                if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                        || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
    
                } else {
                    DisplayMetrics packageMetrics =
                        mView.getContext().getResources().getDisplayMetrics();
                    desiredWindowWidth = packageMetrics.widthPixels;
                    desiredWindowHeight = packageMetrics.heightPixels;
                }
    
    
            } else {
    
            }
    
    
    
            boolean layoutRequested = mLayoutRequested && !mStopped;
            if (layoutRequested) {
    
                final Resources res = mView.getContext().getResources();
    
    
                // Ask host how big it wants to be
                windowSizeMayChange |= measureHierarchy(host, lp, res,
                        desiredWindowWidth, desiredWindowHeight);
            }   
    
        }
    

    上面是删除了很多代码的结果,主要为了说明 performTraversals() 在第一次测量的时候,将建议尺寸填写成了屏幕的宽高,然后调用了 measureHierarchy() 方法。

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
            int childWidthMeasureSpec;
            int childHeightMeasureSpec;
            boolean windowSizeMayChange = false;
    
            if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
                    "Measuring " + host + " in display " + desiredWindowWidth
                    + "x" + desiredWindowHeight + "...");
    
            boolean goodMeasure = false;
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // On large screens, we don't want to allow dialogs to just
                // stretch to fill the entire width of the screen to display
                // one line of text.  First try doing the layout at a smaller
                // size to see if it will fit.
                final DisplayMetrics packageMetrics = res.getDisplayMetrics();
                res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
                int baseSize = 0;
                if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                    baseSize = (int)mTmpValue.getDimension(packageMetrics);
                }
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
                if (baseSize != 0 && desiredWindowWidth > baseSize) {
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                            + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        goodMeasure = true;
                    } else {
                        // Didn't fit in that size... try expanding a bit.
                        baseSize = (baseSize+desiredWindowWidth)/2;
                        if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
                                + baseSize);
                        childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                        if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                                + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                        if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                            if (DEBUG_DIALOG) Log.v(TAG, "Good!");
                            goodMeasure = true;
                        }
                    }
                }
            }
    
            if (!goodMeasure) {
                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                    windowSizeMayChange = true;
                }
            }
    
    
    
            return windowSizeMayChange;
        }

    根据 LayoutParam 取值不同,设置不同的测量模式。主要调用了 getRootMeasureSpec() 方法,然后调用 performMeasure() 方法。

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    可以看到当给 DecorView 设置的 LayoutParam 属性为 wrap_content 时,它的测量模式为 MeasureSpec.AT_MOST,其它的则为 MeasureSpec.EXACTLY,建议尺寸就是该方向上的屏幕宽高对应值。我们再看看 performMeasure() 方法。

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    
        try {
    
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    
    }
    

    一切水落日出,mView 就是 DecorView。ViewRootImpl 调用了 DecorView 的 measure() 方法,启动了整个控件树的测量。

    不过还可以细挖,那就是传递给 DecorView 的 LayoutParams 属性是什么,Activity 是全屏的,所以可以推断它是全屏的。但实际情况是不是这样呢?

    DecorView 的 LayoutParams 实际上是 ViewRootImpl 中的 mWindowAttributes
    ViewRootImpl.java

    final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
    

    WindowManager.java

     public static class LayoutParams extends ViewGroup.LayoutParams
                implements Parcelable {
    
        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }
    }
    

    mWindowAttributes 宽高测量模式都是 match_parent。所以 DecorView 最终测量的建议尺寸就是屏幕的宽高。

    好了,回顾一下,从我们知道 DecorView 是 Activity 最外层的 View 开始,一直在寻找是谁在测绘它,调用它的 measure() 方法。千回百转,一层层追踪下去,都把 Activity、ActivityThread 牵扯出来了。最终找出了源头 ViewRootImpl。

    总结

    如果你一直顺序阅读到这个地方,我要对你说声感谢,谢谢你的捧场。

    此文最终的目的不是为了让你困惑,如果因为篇幅过长,影响了你们的思维,我最后再做一点总结。

    1. View 的测量是从 measure() 方法开始到 onMeasure()。

    2. View 的 onMeasure() 中会接收到 parent 给出的测量规格建议。测量规格包含测量模式和建议尺寸。

    3. 测量模式包含 MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、和 MeasureSpec.UNSPECIFIED。

    4. View 复写 onMeasure() 方法时,一般不处理 MeasureSpec.UNSPECIFIED 测量模式,对于 MeasureSpec.EXACTLY 模式,直接设定 parent 给出的测试尺寸。需要特别处理的是 MeasureSpec.AT_MOST,它需要的是自主决定尺寸数值,但是不得大于 parent 给出的建议尺寸。

    5. View 复写 onMeasure() 方法最后要通过 setMeasuredDimension() 设置尺寸,不然会报错异常。

    6. ViewGroup 自定义比 View 稍难,因为 onMeasure() 方法中要根据子元素来决定自己的宽高,所以要测量子元素的尺寸,通过 measureChildren() 或者根据 measureChild() 方法,然后通过 getMeasureWidth() / getMeasureHeight() 获取子元素期望的尺寸。

    7. Activity 有一棵控件树,DecorView 是它最顶层的 View,但 Activity 相关联的 ViewRootImpl 对象操纵着它们。

    以上。

    展开全文
  • 不用化妆就漂亮

    千次阅读 2012-05-28 11:17:50
    不用化妆就漂亮 1 金霉素软膏,是一种眼药,涂在脂肪粒上可以消脂肪粒 2. 白糖,少量白糖加在洗面奶里洗脸,对去黑色痘印非常有效,有磨砂膏的效果 3. 橙子,晚上洗脸后用橙子皮或橙子果泥涂脸上...

    不用化妆就让你变漂亮


    1 金霉素软膏,是一种眼药,涂在脂肪粒上可以消脂肪粒
    2. 白糖,少量白糖加在洗面奶里洗脸,对去黑色痘印非常有效,有磨砂膏的效果
    3. 橙子,晚上洗脸后用橙子皮或橙子果泥涂脸上,可以使皮肤防干燥,令皮肤水当当的很舒服
    4. 酸奶+香蕉泥,敷脸可以缩毛孔
    5. 氯霉素注射液,用于擦脸可使皮肤平滑
    6. 白煮蛋的蛋白,在鼻子上搓,可以祛除鼻子上的黑头,坚持一段鼻头会很细很嫩, 可以替代去死皮膏
            7. 婴儿油,可以用来卸妆,效果好又实惠
    8. 生姜,削成笔状,用来涂抹淡而稀的眉毛,天天涂一次,渐渐会长浓
    9. 茶包,泡完以后用来敷眼睛,可以消眼袋和黑眼圈,对大眼袋的人很有效,最好在起床的时候做,最好是绿茶或红茶
    10. 洗完头赶时间可以用毛巾包着头发整个吹,干得更快
    11. 指甲油涂完后把指甲浸泡在冷水中,会干的很快速
    12. 食用醋,用来洗烫染受损的头发,洗发冲净后用醋兑水(1:5左右)浇在头发上过5分钟冲掉,再上护发素冲洗,头发就没有那么柴了
    13. 食用醋,双手容易变得干燥粗糙,用食用醋泡手十分钟可护肤
    14. 洗米水,用第2道洗米水洗脸,早晚各一次.5天后皮肤会变得雪白雪白,而且很水嫩
    15. 豆腐,每天早晨用豆腐搓揉面部几分钟,坚持一个月,面部会变得很滋润
    16. 细盐,洗完脸后先不擦干,用细盐搓鼻子,可以去黑头
    17. 草莓,用来涂牙能使牙变白
    18. 鱼肝油,在眼睫毛根部涂抹,可以刺激睫毛生长
    19. 苹果皮,能够治眼部的脂肪粒,用贴的
    20. 葱白薄膜,取出来贴在痘痘上,约15分钟,能去痘印.
    21. 痔疮膏,治疗痘痘,抹平眼角小细纹很有效,原理是痔疮膏的成分和眼霜成分相同,只是工艺和精细程度不同而已

    22. VE,晚上睡觉前涂满嘴唇,第二天早上嘴唇就会粉嘟嘟的,很好用
    23. 红糖+蜂蜜,一勺红糖加半勺蜂蜜磨鼻头,一至两分钟,不可太久,然后洗掉,严重的好象是一星期两次,很有用
    24. 番茄,多吃可以治黑眼圈,吃习惯了熬夜再辛苦都绝对不会有黑眼圈,也不会面有菜色。
    25. 画眼线用眼线液不好掌握的话,可以先用眼线笔画出大概的样子,再用硬头的眼线液描一下,就会很顺手
    26. 长痘痘的人最好不要食用蜂蜜,因为蜂蜜会刺激痘痘的生长
    27. 在珠宝盒中放上一节小小的粉笔,即可让首饰常保光泽
    28. 近视眼造成的眼睛突出,可以采用RGP眼镜来遏制,同时配合一种简单的按摩,坚持一年,可以让眼睛恢复到正常的情况。这种按摩就是把四个手指并起来,让前面一般齐,然后闭上眼睛,在上眼球和眼眶衔接的位置轻轻的往下按,按下去后保持5秒,然后反复。每天按上半小时。效果明显。
    29. 涂增长睫毛的各种增长液也好,油也好,在晚上10点以后涂更有效,因为是睫毛生长的旺盛期。另外,只涂在上半截就可以了,不要涂睫毛根,不然容易长脂肪粒哦。涂到上半截液体可以顺着睫毛流到根部,但不会多到长脂肪粒哦!
    30. 完美的芦荟胶,怎样用都可以,因为是促进皮肤新生的,所以类似于手割破了。。。脸上去斑啊。。。。去痘印啊。。。非常有效很好用,现在晚上洗完脸就擦这 个,感觉皮肤会很嫩,效果明显。被虫子咬了可以去红肿,晒伤了可以镇静皮肤,长痘痘了可以帮助痘痘成熟,之后把痘痘挤了,再把芦荟胶涂上,痘痘很快就好
    --------------------------------------------
    *生活保健类:
    1 肠炎腹泻,吃黄连素片,家中有小孩的朋友,这个肯定对你有用
    2 中暑,一支十滴水+一支藿香正气水合着吃
    3 脚臭,每天用上海药皂洗脚,洗袜子和鞋垫,可以去脚臭。
    4 脚起水泡,可以在睡前在水泡上涂一点加水和好的面粉,一晚上就会好很多。
    5 脚汗,穿鞋的时候,往鞋底里洒点痱子粉,慢慢地,出汗就少了
    6 胃病,多吃一些生姜,生姜同样可以治脚气跟冻疮
    7 小面积皮肤损伤或者烧伤、烫伤,抹上少许牙膏,可立即止血止痛;
    8 腿抽筋,用力捏可能不会很快有效。可以伸直腿,让脚尖回勾,指向自己,能有效缓解抽筋
    9 护嗓,吃西瓜的时候摸点盐,会生成西瓜霜,护嗓子
    10 夏天洗澡用六神的冰片沐浴露,洗好了很滑爽,不会像其他的沐浴露那么又粘又闷还不长痱子
    11 生抽酱油可以用来泡大蒜,一个星期后可以吃了,可以预防感冒,防癌,强身健体
    12 防止生疤,如果是割伤的伤口(很细那种),先把血冲掉在还湿湿的伤口上,撒上绵绵白糖然后贴上传可贴,隔几个小时重新弄点水融绵白糖敷上,基本好了以后没有任何痕迹,尤其对疤痕皮肤的人应该很有用
    13 蚊子咬的包包可以用口水止痒,手边没有其他东西的时候可以试试,效果很好
    14 若在特殊场合不想打喷嚏,在喷嚏即将打出的一瞬间,按住人中穴,喷嚏的感觉会立刻烟消云散
    15 发生头痛、头晕时,可在太阳穴涂上牙膏,因为牙膏含有薄荷脑、丁香油可镇痛
    16  得空经常推小腹,可以照顾到好多穴位并且按摩内脏帮助消化,对身体好
    17 治疗耳鸣,夏天闷热耳鸣时蹲在地上就好了,马上缓解
    18 治疗耳鸣,用手指捏住鼻子,紧闭上嘴,然后使劲吐气,让气从两个耳朵出去,快速有效
    19 夏天擦拭凉席,用滴加了花露水的清水擦拭凉席,可使凉席保持清爽洁净。当然,擦拭时最好沿着凉席纹路进行,以便花露水渗透到凉席的纹路缝隙,这样清凉舒适的感觉会更持久
    20 睡眠不足会变笨,一天需要睡眠八小时,有午睡习惯可延缓衰老
    21 吃了辣的东西,感觉就要被辣死了,就往嘴里放上少许盐,含一下,吐掉,漱下口,就不辣了;
    22  牛黄解毒片不能多吃,会重金属中毒。
    23 板蓝根可以将体内死去的垃圾细胞排出去!有生病为了快速见效而用抗生素的朋友,可以使用这个方法.
    --------------------------------------------

    *生活清洁类:
    1 吹风机对着标签吹,等吹到商标的胶热了,就可以很容易的把标签撕下来
    2 巧除纱窗油腻:将洗衣粉、吸烟剩下的烟头一起放在水里,待溶解后,拿来擦玻璃窗、纱窗,效果不错
    3 闲置的润肤霜、乳液之类的东东,扔了可惜,可以用来擦包包(皮质的,或者仿皮的都可以),一擦就干净像新的一样
    4 喝过没气了的可乐,可以用于清洁马桶,倒在马桶里过10分钟冲掉,你会发现马桶很白很干净
    5 风油精驱除污渍,特别是原珠笔印,一擦就没了
    6 用醋擦家里的水笼头、不锈钢水池等,用的时间长了就变得不够光亮了,用这个办法一擦就很亮了
    7 用丝袜清理不锈钢用具,省力又清洁,保证光亮如新。
    8 如果衣物上不小心滴上了油,在洗衣服之前,保持衣服是干的,然后在滴油的地方滴上洗洁精,再搓两下,洗的话就不会有油印子了。
    9 用牙膏和牙刷,刷边黑了的银质东西,会使其变得好亮
    10 发霉有了霉点的衣服,把牙膏涂在上面,干后再洗,可以洗得干干净净
    11 洗纯白色的衣物(不能是乳白色),用清水投洗的时候,先在水里加一点点(一定要是微量,加多了就玩完了…)蓝色的钢笔水,水变成淡淡的淡淡的蓝色,然后把衣服放进去投洗。这样洗出来的衣物,在视觉上会给人感觉更白
    12 晒白色运动鞋的时候外面裹一层卫生纸,可以让鞋更白
    13 将白醋喷洒在菜板上,放上半小时后再洗,不但能杀菌,还能除味
    14 往墙上贴画不管用胶水、浆糊、双面贴还是胶带,都会留下痕迹。想不留痕迹,就挤出花生米大的牙膏(别选有色的)抹在海报的四个角上,使劲一按。日后能轻而易举的揭开,而且不留痕迹,白色的墙面一点也不会弄脏
    15 有血迹的衣服,无论什么质量的布料,把面霜(任何品牌都可以)涂在血迹上,放10分钟,然后用肥皂or洗衣粉等撮洗
    16 杯子上都有茶渍,可以用牙膏刷,抹在茶渍上,一刷就下来
    17 夏季洗衣服前,把衣服用花露水加入温水里浸泡,尤其是鲜艳颜色的T恤,多泡一会,那样可以保持颜色鲜艳,并且味道很好闻
    18 颜色深可能会掉色的衣服,新买回家的时候,可以用一点盐加清水泡半个小时再洗,就不容易掉色了
    19 电冰箱冷冻柜内壁擦干净,用食用油沾在布上,均匀的擦在冷冻室四周的内壁,以后再除霜的时候就非常方便,轻轻一剥冰就掉了
    20 煮完面条的汤,用来刷碗会非常干净
    21 如果手头没有洗洁精,可以拿牙膏代替来清洗油腻的碗和盆,一样干净
    22 新买回的铁锅,用淘米水泡上至少1-2小时,用起来炒菜很好,不会粘锅也没有铁腥味
    23 吸盘式的吸墙钩或是架子,在吸盘上均匀地涂一点点色拉油,然后用力按到墙上,会吸得更稳
    24 洗锅的清洁球(那种细钢丝的),用得很脏很脏后,不要扔掉,把它把火上烧。污垢会变成碎杂掉下来,又是一个新的清洁球了
    26 女孩子的丝袜比较容易被锐物刮破,买回来后放在冰箱冷藏里冻上一会儿,再穿的话就不是很容易破了
    27 在室内放浸泡了洋葱的冷水可以去除油漆味儿,刚装修的房子可以放置一些柚子皮 ,生间也可以放,祛除异味
    28 将残茶浸入水中数天后,浇在植物根部可促进植物生长;把残茶叶晒干,放到厕所或者沟渠里燃熏,可消除恶臭,具有驱除蚊子苍蝇的功能
    29 插花时,在水里滴上一滴洗洁精,可以多维持花的寿命
    30 如果衣服上不小心沾上番茄汁或其它果汁,马上用湿纸巾擦,基本可以去掉不留痕迹
    31 笔记本电脑不小心洒到了水或是油,可以暂时先用吹风筒吹下,然后把电脑关机,在没送到维修之前千万不要再开机,这样利于修理
    32 书或是资料被水弄湿,稍稍擦干,然后用厚重的书挤压好,套上塑料袋 放进冰箱的冷冻层,半天后再拿出来 ,纸张会变成原来的样子
    33 帆布鞋清洗时,用柔软的布或丝袜,配合洗洁精,洗的干净不伤鞋
    34 皮包上有污渍,可以用棉花蘸风油精擦拭,效果不错
    35 夏天擦拭凉席,用滴加了花露水的清水擦拭凉席,可使凉席保持清爽洁净。当然,擦拭时最好沿着凉席纹路进行,以便花露水渗透到凉席的纹路缝隙,这样清凉舒适的感觉会更持久
    36 玻璃上有残留双面胶的胶印的,用纸巾蘸洗甲水,在胶印上一擦,胶印马上消失
    37 扫地的时候,地上有毛毛球球之类的扫不干净。如果家里有烂洞洞的丝袜,别扔,把丝袜套在扫帚上用水弄湿。再扫地的时候,毛毛球球、浮灰啥的都会粘在扫帚上,很干净很方便
    38 蜡烛冷冻二十四小时后,再插到生日蛋糕上,点燃时不会流下烛油
    39 不管是鞋子的哪个地方磨到了你的脚,你就在鞋子磨脚的地方涂一点点白酒,保证就不磨脚了
    40 塑料东西如果断了可以在断口上抹上风油精,拼在一起个一两分钟就合在一起了。
    41 出门时随时在包里带一节小的干电池,若裙子带静电,就把电池的正极在裙子上面擦几下即可去掉静电
    --------------------------------------------
    *生活饮食类:
    1 炒茄子前,用开水焯一下或用盐腌出苦水,这样就不吸油了
    2 芒果在煤气炉上转几秒,那皮就很好剥了,番茄剥皮,用开水烫下,皮就容易剥下来了
    3 普通的铁锅,需要煎东西时,先把锅放在火上加热,倒少量油,油热后倒出,再倒入冷油,就变成不粘锅了,煎鱼、水煎包都不粘
    4 炖排骨或汤的时候,放点醋,有利于钙的吸收
    5 喝不完的大瓶装可乐,可以到过来放比较不容易漏气
    6 蒸米饭,在锅里加几滴生油。搅一搅。蒸出来的饭一粒一粒的,很好吃。而且不粘锅。
    7 做猪扒或者牛扒的时候,先将肉块用口乐浸泡10分钟再煮或煎,肉质会容易酥软美味.也不会残留可乐的味道
    8 洗黑木耳的时候放一点点面粉,会洗下很多脏东西的
    9 剥大蒜之前,用水把整个蒜头泡过,去皮就很容易了。
    10 煮饺子时要添足水,待水开后加入2%的食盐,溶解后再下饺子,能增加面筋的韧性,饺子不会粘皮粘底,汤清饺香
    11 把核桃放进锅里蒸十分钟,取出放在凉水里再砸开,就能取出完整的桃核仁了
    12 将鸡蛋打入碗中,加入少许温水搅拌均匀,倒入油锅里炒时往锅里滴少许酒,这样炒出的鸡蛋蓬松、鲜嫩、可口。
    13 过多食用生葱蒜,会刺激口腔肠胃, 不利健康,最好加一点醋再食用
    14 把虾仁放进碗里,加一点精盐和食用碱粉,抓搓一会儿后,用清水浸泡然后冲洗,炒出的虾仁会透明如水晶,爽嫩可口
    15 做啤酒鸭,红烧猪蹄之类的菜式,在加水焖的时候,从炒锅里倒进电饭煲里面焖,肉更嫩、更香、更滑、水汽还少,特别好吃
    16 洗桃子的时候 ,给水里放点碱面,桃毛自然都漂在水面,而且洗完手还不痒 ,这样洗桃绝对干净
            17 喝酸奶,能解酒后烦躁,酸奶能保护胃黏膜、延缓酒精吸收,并且含钙丰富,对缓解酒后烦躁尤其有效
    18 炒肉时,先把肉用小苏打水浸泡十几分钟,倒掉水后再入味,炒出来会很嫩滑
    19 洗葡萄的时候放些淀粉,随便搓一搓,就完全能把葡萄洗得亮晶晶了
    20 红薯擦成丝状,炒出来味道也很独特,通肠的效果很好
    21 用刀切粘乎乎的东西(年糕、粑粑类)前先把刀淋过冷水,就一点都不粘了
    22 夹生饭重煮法:可用筷子在饭内扎些直通锅底的孔,洒入少许黄酒重焖,若只表面夹生,只要将表层翻到中间再焖即可
    23 如何鉴别鲜蛋:新鲜蛋用灯光照,空头很小,蛋内完全透亮,呈桔红色,蛋内无黑点,无红影。若是要测量蛋的新鲜度,可以将蛋浸在冷水里。如果蛋是平躺在水 里,说明十分新鲜;如果倾斜在水中,则该蛋至少已存放了3~5天了;若是笔直立在水中,可能存放10天之久。此外,若是蛋浮在水面上,那么该蛋十分有可能 已经变质了。
    24 吃过于肥腻的食物后喝茶,能刺激自律神经,促进脂肪代谢
    --------------------------------------------
    *其他小常识:
    1 脚麻了的时候把食指放舌尖沾点口水,然后再把口水点到鼻尖上…重复这个动作三次就好了
    2 吃鱼的时候卡住了,可以把一个空碗放在头顶上,在拿双筷子,对着碗心敲敲敲拼命的敲,往死里敲````
    3 有沙子或异物进眼睛的时候,立即吐口水,吐多点,然后不停地眨眼睛,百试百灵的

    4 砂子进了眼睛千万不要用手揉或者转动眼珠,马上把进了砂子的那只眼睛闭上,然后咳嗽再张开眼睛,马上就会好
    5 夏天洗澡的时候,在发泡的沐浴球上洒上风油精会很凉爽
    6 被蚊子咬,最快的方法是用香皂沾水后涂一下
    7 下雨天路滑,如果在鞋底两端各贴上一片创可贴就不会滑倒了.
    8 被辣椒辣手,倒点白酒涂患处,擦3-5分钟就好了

    9 仰头点眼药水时微微张嘴,这样眼睛就不会乱眨了;
    10 眼睛进了小灰尘,闭上眼睛用力咳嗽几下,灰尘就会自己出来;
    11 吃了有异味的东西,如大蒜、臭豆腐,吃几颗花生米就好了;
    12 打打嗝时就喝点醋,立杆见影
    13 说到打嗝,如果身在外面,没东西怎么办呢?我告诉大家,按手心! 随便哪个手心都行,用一只手的大拇指拼命的按住另一只手的手心。对打嗝有奇效,按下就不打了。这里有个穴位,按得准,按个十几秒,半分钟的,松开手了也不 会再打。按得不准,松手后马上接着打, 那就再按吧。 要使劲哦!
    14 吃完大蒜后,喝一杯牛奶,牛奶中的蛋白质会与大蒜发生反应,就可以有效去除蒜味了,喝牛奶时,注意要小口慢咽,让牛奶在口腔中多停留一会儿,而且最好喝温牛奶,这样会更好。
    15 吃完桃子,把桃核洗干净,晾干,积攒几个,包起来,放在枕头下面,可以睡得安稳。也就是避邪了啦,防止“鬼压床”之类的*~其实这个不算小常识啦,有点迷信,但是信则无,桃树枝和桃核都是可以辟邪的哦,小孩子手上带个桃核的小饰物对孩子好^_^

    展开全文
  • 他和我关系可好了),这位老中医以前是我们这边省中医院内科的专家,退休了返聘。。。到78岁时候自己创业开了一家中药铺,就在我家楼下。  本来我是想问问他要不要吃点中药的,后来他跟我说了一个小办法,根本...
  • 不定内存池之apr_pool

    千次阅读 2011-01-19 22:51:00
    不定内存池 apr_pool
  • 解决这个问题有两种思路,一是在“预处理”的环节中采取“矫形”措施,使汉字字形变得较为工整。另一种途径是精心选择识别特征,使同一种汉字不同样本的特征,其差别尽量小;而不同汉字的特征,其差别则尽量大,这跟...
  • 是python的编译器,如果我们没有编译器,写代码是很困难的,而且pycharm提供了许多插件和优美的界面,使得你写代码舒服,不会因为在python的IDE上写头疼。 当然!下载软件不用到官网下,你只需要到软件管家...
  • OpenCV从入门到放弃(三):Core组件

    万次阅读 2016-09-07 16:33:57
    Core组件
  • 刚来北京时,去农业银行办理银行卡,因为要挂号依次办理,等了大约...办理我银行卡的时候,银行工作人员递过来单子,要签客户名,mm就在我右边,我的竟然抖了起来,我努力去控制,可越不它抖它越抖!无法正常签名了,mm似乎看
  • 数处女座的101个特质

    千次阅读 2013-11-27 15:30:19
    连饭都没吃,谈什么感情。  其实如果身边有处处男的异性友人,你有什么打算啊计划啊,不如多和他聊聊,他在讨论别人问题时,通常非常清醒、且有条理,极细致,可以帮你很大的忙,补足你忽略的一些问题。  ...
  • 上的商学课-老路

    千次阅读 2019-05-12 18:19:32
    3.消费者调整需求的时间是还是短:短说明弹性小,反之越大。 第八章:凡勃伦商品 越贵越买,不是“傻”,而是“壕”。价格高的不一定东西好,更可能是因为这件东西你自我感觉良好。经济学家认为商品包含两...
  • 读《增长黑客》有感

    万次阅读 2020-06-30 17:46:14
    《增长黑客》作为互联网产品策划必读书籍,详细介绍了如何产品发展更好,并列举了各种案例和方法论。笔者作为技术人员,阅读《增长黑客》后整理笔记于此。忙碌的朋友们可以从本文看看是否有兴趣和必要去拜读下...
  • 该材料吸收人体自身的细胞,并最终转化为活组织 一位阿尔茨海默氏症(老年性痴呆)的钩编作品,渐渐变得不成形 这孩子肯定没有被抱错!(出生前三周和出生后三周对比) 靴子还是那只靴子,狗狗还是那只狗狗,不过...
  • 《增长黑客》节选与笔记

    万次阅读 2019-10-04 05:21:14
    这一年,从炎夏写到寒冬,从“魔都”写到“帝都”,每一个加班过后疲惫不堪却必须振作起来赶稿的子夜,每一场春光烂漫却无暇漫步的周末,它们都成为了我甜蜜的“噩梦”,一边吞噬着我的每一颗脑细胞,一边我...
  • TCP是面向字节的:发送方TCP对应用程序交下来的报文数据块,视为无结构的字节流(无边界约束,可拆分/合并),但维持各字节流顺序(相对顺序没有),TCP发送方有一个发送缓冲区,当应用程序传输的数据块太,TCP...
  • yolo系列之yolo v3【深度解析】

    万次阅读 多人点赞 2018-09-12 16:24:48
    看过yolov3论文的应该都知道,这篇论文写很随意,很多亮点都被作者都是草草描述。很多骚年入手yolo算法都是从v3才开始,这是不可能掌握yolo精髓的,因为v3很多东西是保留v2甚至v1的东西,而且v3的论文写很随心。...
  • 原文地址:Unity游开发札记——布料系统原理浅析和在Unity游中的应用0. 前言项目技术测试结束之后,各种美术效果提升的需求逐渐成为后续开发的重点,角色效果部分的提升目标之一便是在角色选择/展示界面为玩家...
  • 智能手环一些模块总结

    千次阅读 2018-02-01 19:21:03
    缺点:由于心电信号的波长非常,为了测足够精度的信号,信号电级和参开电极必须在躯干空间上隔足够远,一般是胸上比较远的两点,或者左手和右手,腕表比较难采用这个方案。 1.3动脉血压法 最古老方法之...
  • 游设计分辨率推荐

    千次阅读 2017-08-25 10:54:22
    我也花了很时间才弄明白,感觉有必要写一篇足够通俗易懂的教程来帮助大家。从原理说起,理清关于尺寸的所有细节。由于是写给初学者的,所以不要嫌我啰嗦。 推荐配合这个阅读:《 最新Android & iOS设计尺寸规范 》...
  • K线虽然还是那些K线,波浪虽然还是那些波浪,但从这一刻起才会真正变得鲜活、生动起来;招数虽然还是那些招数,也只有从这一刻起才能对你有实际意义。在质变的临界点没有达到之前,即便你满腹经纶、绝顶聪明,被人性...
  • LINUX系统移植(史上最全最,强烈推荐)

    万次阅读 多人点赞 2011-08-16 16:42:36
    Linux系统移植 目 录 第一部分 前言.....................................................................................................................
  • 日语动词变形总结

    千次阅读 2013-10-07 11:34:27
    (一般用来て)“因为小林来了,所以变得非常热闹起来。” C,名词法:一些动词连用形=表示动作的名词  a,单独的名词  ① 変なことを言って笑いの種になりました。“说了不得体的话,被人当成了笑料。...
  • 全息经络诊法

    千次阅读 2006-03-07 14:01:00
    故将“全息经络诊法”附于此处,以供大家参考习用。 “望诊病”起源于民间,历史悠久。古今中外,研究者甚多,因其方法简便易学、准确实用而长盛不衰。望诊病的流派很多,各有所长。我国流传的望诊病方法多...
  • 怀着满心的科幻愿景,习惯了滕导演以及各位演员不遗余力地前期宣传,我得以熟知了这一部中国式的科幻大片《上海堡垒》,最初看到新闻消息说,... 20190809上映,受到台风“利奇马”的袭击影响,我还未来及走进...
  • 如果由于某些原因,我们引入了达瑞尔,并付给他足够买9辆保时捷的薪水,而他一旦干不够好,就不会在这里工作很久... ...但这些开发人员都是精心挑选出来的人才......唯一的不利之处在于,总有一部分人感到开发...
  • 今年的冬天,比以往的没有寒风凛冽,没有下过一场冬雨,城市里没有花草树木的陪伴,背井离乡异地为伴,等待的是父母的老去,自己真的变得成熟么?      今年是写博客的第...
  • Java 并发工具包 java.util.concurrent 用户指南

    万次阅读 多人点赞 2015-03-03 09:40:29
    这个包包含有一系列能够 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现自己的相关工具类。本文我将带你一一认识 java.util.concurrent 包里的这些类,然后你可以尝试着如何在...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,737
精华内容 18,294
关键字:

如何让手变得又细又长