精华内容
下载资源
问答
  • 滴滴出行活动策划、用户成长体系、用户增长逻辑分析1功能模块分析及产品介绍1.1功能模块1.2产品介绍2活动策划(以愚人节为例)2.1活动主题2.2活动目的2.3活动目标2.4活动资源2.5活动对象2.6活动规则2.7活动时间2.8...

    1功能模块分析及产品介绍

    测试机型:小米5A
    测试版本:5.2.8
    测试时间:2018年10月

    1.1功能模块

    在这里插入图片描述
    从滴滴出行APP的整个结构来看,一共包含3个部分:打车服务、个人中心和更多功能。其中打车服务主要包括快车、出租车、拼车、代驾、顺风车、公交、二手车、豪华车、自驾租车、专车和企业级用车等功能,个人中心包括用户头像、等级、订单、钱包、客服、设置等功能,更多里面则包含滴滴的一些产品、合作企业、用户和反馈等相关功能。从维度上来讲,主要包含车主、乘客两大模块,如果继续细分,乘客可以分为女性用户、学生用户和普通用户。

    1.2产品介绍

    (1)产品介绍(官网)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (2)产品功能
    打车:包括出租车、快车、专车、豪华车、公交、小巴、代驾、企业级、共享单车、共享电单车、共享汽车等。

    2活动策划(以愚人节为例)

    愚人节(April Fool’s Day或All Fools’ Day)也称万愚节、幽默节,起源于西方,但随着中西方文化的交流与传播,也在我国流传开来。因为其独特的戏剧性和商业性,各大商家纷纷借势营销,以提高产品的知名度、用户参与度和用户活跃度,当然滴滴出行也不例外。下面以滴滴出行为例对愚人节活动策划做简要分析。

    2.1活动主题

    偶遇“大明星”

    2.2活动目的

    进一步提高新一线城市的拉新工作

    2.3活动目标

    活动日新一线城市新增30w用户

    2.4活动资源

    0.41元,4.1元和免单红包(具体数量未知)

    2.5活动对象

    所有滴滴用户

    2.6活动规则

    所有红包只能下次乘车使用,有效期为7天,其中,免单红包不得超过20元,支付金额超过的则不可用该红包。

    2.7活动时间

    仅限4月1号当天

    2.8活动推广

    在这里插入图片描述
    在这里插入图片描述

    2.9活动平台

    在运营时,首先要确定的是此次运营的目的是什么,如果是为了提高KPI且可活动资源有限,此时需要对用户做RFM和28法则分析,把有限的资源最大化,如果是为了拉新,就应该做活动吸引新用户注册并完善个人信息。在运营完成后,需及时复盘,查看最终结果与预期目标的差距、投入产出的最终比例,以及运营前后数据的变化等。通过复盘,提升运营活动质量的持续性。

    3用户成长体系

    3.1概念

    用户成长体系是**【运营手段】和【产品机制】最好的结合体,是运营与产品相得益彰的体现,主要目的是促活、留存**,它会让一个产品更加的完整和饱满,如果用户成长体系做的足够成功,那将会产生巨大的效益:
    产品来说:提高用户黏度。所谓成长,一定附带着“习惯”和“依赖”;同时也促进了产品的成熟。
    运营来说:在不同的成长阶段有一套对用户标准的分类,有助于实现用户的分级,也能更好的进行精细化运营的操作。
    商业来说:一个完整的、有用户体系的产品会比一个尚未成熟的产品更具有商业价值。通过用户运营,能找到合适、可持续的商业模式;产品价值高,也更能够获得资本的支持。
    用户来说:用户成长体系是产品游戏化的体现,有助于引导用户使用产品功能、满足用户成就感,尊享更多更好的特权,能够从中获取利益、荣誉、安全和情感等需求。
    并不是所有产品都适合,或者都需要设计用户成长体系的。面对需求非常强烈,你不需要激励用户,他们都会投欢送抱的产品,就不需要画蛇添足。譬如微信的即时通讯(聊天)功能,它的功能就十分简单,8亿用户依然没有犹豫地选择用它。
    还有一种不适合做用户成长体系的产品,就是低频产品,譬如婚庆、丧礼、毕业典礼相关的服务和商品,一生只有一次或几年才有一次使用场景的东西,你非要鼓励用户去多用,就不太懂人情了吧。
    下面以滴滴出行APP为例,分析其“用户成长体系”。为了提高用户活跃度和留存率,滴滴在这个方面可谓是下了不少功夫,围绕其核心业务—打车,滴滴出台了2套用户成长体系,一个是通过打车获取滴币去积分商城兑换相应的礼品,另外一个是通过打车(不包括出租车)累积里程数升级为不同等级会员,获取不同的会员特权。

    3.2滴币(积分)体系

    (1)入口
    打开滴滴的APP首页,选择或输入我要去哪,然后直接点击 呼叫出租车 等待上车,在到达目的地之后,滴滴会自动计算你的费用和里程,并折算成滴币,计入到滴币和会员成长体系中。
    在这里插入图片描述在这里插入图片描述
    (2)出口
    在获取到一定的滴币后,用户可以根据自己的需要去积分商城兑换相应的商品。
    在这里插入图片描述
    (3)获取规则
    滴币的获取规则如下图所示,不同车型在同样里程数的情况下有所差别,且只有出租车、专车和快车能兑换滴币,根据打车费用折算成滴币,并且设置了单日最高限额,这主要是为了防止恶意刷滴币。
    在这里插入图片描述
    (4)特权
    在滴币体系中,好像没有特权一说(可能没找到),只要你的积分够,就可以根据自己的实际需要兑换相应的商品。
    (5)回收机制
    由于在写此文章时,积分商场存在异常,所以无法看到相应规则。

    3.3会员体系

    在会员体系中,滴滴还对用户进行了分层,分别为快车和礼橙专车,其对应的会员机制也存在轻微差别,不过会员等级都一致,分别为积分会员、白银会员、黄金会员、白金会员、钻石会员和黑金会员。

    3.3.1快车

    (1)入口
    快车会员体系的入口和滴币体系大同小异,只需将上面一栏拉到快车,即可呼叫快车。
    (2)出口
    在快车里,用户在会员等级不断上升的过程中只是相应的权限发生了变化,其累计里程数并不能兑换相应的商品。
    (3)获取规则
    不同的会员等级所要求的一季度累积里程数不一样,并且对于不同的出行方式换算方式也不一样,可能是为了提高差异化吧,鼓励用户使用优享车(基于滴滴快车和滴滴专车之间提供的一种比专车价格更便宜但是舒适度更高的出行方式)。
    在这里插入图片描述在这里插入图片描述
    (4)特权
    针对不同等级的用户,也将拥有不同的等级特权,但是在快车中钻石会员和黑金会员享受的特权是一模一样,这样是不是显得没有区分度了?是不是会减弱钻石会员用户使用滴滴出行打车的刺激度?
    在这里插入图片描述
    (5)回收机制
    滴滴快车会员采用里程累积来提升会员等级,每单结束后系统实时记录增加的里程数,并且一个季度清零一次。在一个季度内,如果累积里程数达到高于本会员等级的要求,则升级并享有相应特权,直到下季度末;否则下个季度做降级处理。(说实话这个会员体系是看了好一会才明白,有点复杂绕弯,难理解)。
    在这里插入图片描述

    3.3.2礼橙专车

    (1)入口
    专车会员体系的入口和滴币体系大同小异,只需将上面一栏拉到专车,即可呼叫专车。
    (2)出口
    可能考虑到用户等级上升并没有带来一些实际利益,在礼橙专车里,加了一个里程兑换商城,不过目前还没有正式上线。
    在这里插入图片描述
    (3)获取规则
    不同的会员等级所要求的一个月累积公里数不一样,具体如下图所示。不过说实话这个地方有点蒙,到底是按公里or里程计算?和快车不同的是,专车只有5个会员等级(快车6个),并且对应的里程数要求也不一样,同时统计口径专车为一个月(快车为一个季度)。

    在这里插入图片描述
    (4)特权
    针对不同等级的用户,也将拥有不同的等级特权。针对黑金用户,溢价保护升级为免溢价权益,优先派单升级为极速应答权益。
    在这里插入图片描述
    (5)回收机制
    滴滴会员采用里程累积来提升会员等级,每单结束后系统实时记录增加的里程数,并且一个月清零一次。除黑金会员等级可保护3个月以外,其余等级每月月初重新根据里程数进行会员定级,同时本月里程从0开始累积。
    在这里插入图片描述

    3.4滴币和会员体系总结与疑惑

    从上面的分析可见,滴滴出行的滴币体系和会员体系两者是相互独立的,并不存在牵制的关系,下面对2种用户成长体系做一下简单总结:
    获取途径:出租车、专车、快车可以获取滴币兑换商品,但是只有快车和专车(不包含出租车)才纳入到会员升级体系中。
    等级体制:滴币体系并不存在用户等级划分,有多少积分就可以在积分商城兑换相应的商品;会员体系存在等级划分,不同等级的用户拥有不同的特权。是不是可以限定不同会员等级的用户去积分商城兑换不同商品的权限,以刺激用户消费?
    特权体制:滴币体系没有特权设置,会员体系中不同会员等级用户拥有不同的会员特权。
    问题与疑惑?
    (1)积分商城中可兑换的商品稀少,大多为电商引流所用,用户使用滴滴打车所获取的滴币并没有足够体现出相应的价值,显得有点诚意不足。要是在商城中引入更多的关联商品(可以在意见反馈中搜集意见),并能成功吸引用户购买欲望的话,这个滴币体系会不会显得更加有活力?
    (2)滴币体系和会员体系的所包含的车型存在差异,并且会员体系中专车和快车特权也不一样,考虑到呼叫专车不能换取滴币,所以在专车新增了一个按照里程数兑换商品的里程兑换商城,对于用户来讲,这样设计是不是显得有点过于复杂难懂?是为了提高差异化?
    (3)会员体系中的快车和专车等级体系和权限完全是2种不同的体系,升降级规则(要求的里程数)不一样,统计口径也不一样(快车为一个季度,专车为一个月),并且在专车会员规则中,进度条是公里数,下面的说明又是里程数,到底按哪个标准走,公里数和里程数又如何换算(在出行方式换算中只包含出租车、快车和优享车,并没有包含专车)?
    (4)会员体系中的快车会员好像是没有出口的,即用户拿到快车会员之后除了享有相应的特权之外并没有其他用途,不像专车还有里程兑换商城。仔细一想发现在打快车时,既可以获得滴币,同时又可以累积里程数,是不是可以理解成快车会员的出口实际上在积分商城里?
    (5)滴币体系没有等级体制,有多少积分就可以兑换相应的商品,是不是可以通过限制会员等级来限制兑换的商品?但会员等级只有专车和快车有,滴币是快车、出租车和礼享车有,会不会显得无法实施?
    (6)滴币体系用户可能为了换东西,会激发用户打车的欲望,但是会员体系好像就是我打了一个车,然后你就给了我相应的会员,激励作用何在?也许会员体系有很多东西是我不知道的,此时只是说一下自己对这个东西的理解。
    总的来说,滴币体系满足了人群的利益需求(虽然诚意略显不足),会员体系满足了人群的荣誉需求。【是不是可以通过发送节日祝福短信满足人群的情感需求,在每次呼叫快车时弹出乘车安全注意事项(特别是夜里或者女生打车时)满足人们的安全诉求,以提高用户体验度,抓住用户,这一点可以尝试。】

    4用户增长

    要想实现用户增长,需要对业务进行梳理,拆解出增长点与增长方式,并通过实验测试与数据分析驱动产品的快速迭代,找到增长方向,为用户传递产品价值,实现增长目标。如果要实现从0-1的蜕变,则需要做到以下几点(以北京回龙观滴滴小巴为例做相关探讨):

    4.1用户增长逻辑

    4.1.1明确需求

    需求就是什么人在什么时间、什么地点会使用你的产品。比如说我13:00在王府井打出租车去公司了,这就是需求。
    目标群体定位(何人):
    在回龙观等车、坐车的所有用户,包括上班族和学生。
    用户场景定位(何时、何地):
    用户场景的选择需要满足两个条件,一是必须要有需求,最好是高频需求,其次就是能直击用户痛点,比如说我在公交站下班等车想要回家(需求),但是我要乘坐的那班公交车半小时一趟,且每次到达这个公交站时公交车上人都快满了,还得冲着挤上去,所以每次搞得我心情很不愉快,要是我能很快坐上车并且不用挤,还能坐着回家就好了,就算多出点钱也无所谓,毕竟这样舒服啊(痛点)。滴滴小巴为什么将测试点选在了回龙观,一个原因是回龙观无论高峰平峰平日周末需求都很稳定,即满足了稳定需求的条件,二是观内交通不好,公交痛点明显,也满足了直击用户痛点的条件。

    4.1.2种子期(冷启动)

    所谓冷启动,指的是在产品初期,从目标用户转化为种子用户(需要对产品进行很好的诠释)的过程。在冷启动阶段最好的方式是地推,与用户沟通交流,发现问题,最后解决问题。在回龙观,滴滴小巴的目标用户就是从地铁口出进、在公交车站下车和等待上车的人,通过给他们发传单让他们了解并使用我们的产品,转变成我们的用户。在这个时候需要考虑三个问题,即我们给用户推荐的产品是什么?我为什么要用你的产品?我为什么非要用你的产品(从利益、安全、情感、荣誉这些角度出发,才能有效的吸引用户)?在给出这三个问题的答案之后,用户才有可能使用你的产品,从而成为种子用户。种子用户往往好奇心强,敢于尝试。
    种子期三件事:1.一定要跟用户多沟通,根据用户反馈不断对产品进行改进;2.100用户和1000用户没有任何区别;3.目的就是为了做一个留存漏斗,验证需求。
    种子期的关注点:留存率(周、月、3月等都是统计口径,视具体情况而定)是本质。

    4.1.3爆发期

    种子期结束、爆发期开始的标志是什么?种子期核心看留存,留存率达到一定的阈值就说明产品成功度过了这个阶段。当用户月留存率大于10%的时候,恭喜你放手去干吧,爆发期就要来了。
    所谓爆发期就是想尽一切办法做拉新,要想做拉新,就要做推广,提高品牌曝光度,让更多的用户知道你的产品,推广方式包括线上(微博、微信、QQ、搜索引擎、应用商店)和线下(地推、软件预装)等。在前期,如果资金允许管他三七二十一就是往各个渠道疯狂砸钱,如果资源有限,就先小规模大范围投放,通过埋点或第三方统计软件分析各渠道来源用户的质量如何,然后对优质渠道进行精准投放。
    在此阶段需要关注的三个指标分别是CAC(用户获取成本),LTV(用户生命周期价值),PBP(回收期)。
    爆发期的关注点:找获客天花板同时关注用户活跃度(后期)。

    4.1.4瓶颈期

    当爆发期结束后,就进入瓶颈期了,这个阶段更关注是用户活跃度的指标。爆发期的结束的时候我们发现获客成本非常高,碰到天花板。那么就需要通过更好的产品及更赞的服务来提高用户活跃度,以此提高获客天花板。
    举个例子,假如用户活跃度是平均日活跃1小时,拉新成本50元一个,现在活跃度变成2个小时,拉新成本就不是50元了,因为用户生命周期价值翻倍了,所以拉新成本就变成了25元。
    度过瓶颈期可以帮助我们进入下一轮的爆发。因此整个增长的逻辑就很清晰了:需求、种子期、爆发期、瓶颈期,在后期会出现“爆发期“、“瓶颈期”交替迭代。这就是增长的逻辑。当大势所趋,你需要跳出增长,关注如何盈利、以及如何保持稳定规模。

    4.2用户增长方式

    在产品初期,通过地推、调研和用户沟通不断的改进产品以提高用户留存率,一旦有了一定的留存率(高于10%),就说明你的产品是有需求的,此时可以准备爆发期的来临,大量投放广告,以提高品牌曝光度,拉进大量的新客户,有了客户之后就需要好好改善产品提升用户体验和活跃度,用户有了好的体验之后就会形成口碑互传,此时都不需要做太多的产品宣传,用户自然而然就会来。要是用户活跃度太低,可能是用户用腻了,缺乏吸引力,此时需一边改善产品,一边推出新功能、新玩法,完成爆发期和瓶颈期的交替迭代。
    各时期最有效的手段和最需要关注的指标(黄色部分)如下所示:
    在这里插入图片描述
    整个种子期最核心的事情还是需要关注“留存”,需要通过优化产品得以实现,种子期拉新最有效的办法还是地推,最有效的活跃办法是调研,不要给用户发券,只需要跟用户聊聊天,因为种子期往往吸引的是新鲜用户,这些用户也最能给你反馈最有价值的信息,这才是种子期促活非常关键的一件事。
    爆发期核心是拉新,你需要建立更多的渠道,关注渠道的转发率、获客收益。爆发期最好的促活办法就是搞促销,留存最好的办法就是做调研,召回最好的办法还是跟用户聊聊天。
    瓶颈期,最应该关注的是活跃度,应该通过产品和效率模型来进行提升。瓶颈期的增长并不需要爆发期那样的趋势,需要更多的是稳定。当然还应该修炼内功,多做品牌,在市场上发声,提高口碑,挖到一些新的需求点。瓶颈期留存最好的办法是做会员和积分。

    5滴滴出行的AARRR模型

    5.1前言

    AARRR模型,通俗的理解就是,用户怎么来,用户来了怎么活跃,用户活跃之后怎么留存,用户留存之后怎么为产品付费,用户付费之后怎么进行口碑的传递。本文将结合AARRR模型对滴滴出行APP做一些简单的工作和学习总结,以下只是个人拙见,欢迎大佬多多指教。

    5.2AARRR模型定义

    AARRR模型一共包含5个部分:获取用户(Acquisition)、提高活跃度(Activition)、提高留存率(Retention)、获取收入(Revenue)、病毒式传播(Refer)。
    在这里插入图片描述
    作为一个刚刚在市面上浮现的产品,虽然在发布初期已经在公司内部做过很多测试,包括技术、功能、A/B测试等,但是发布到市面以后用户是否买账,是否符合用户喜好,解决了用户的需求,这一点还有待考验。

    5.2.1获取用户

    公司发布一个新的产品,其目的就是为了解决市场或者用户的需求,最终达到盈利的目的。因此在产品发布前期的首要任务就是获取用户,要想获取用户需要做大量的推广,以提高产品的曝光度和知名度,让更多的用户知道并了解你这个产品。具体的推广渠道如下:包括社交广告、搜索引擎与SEM、社交媒体、软件商店、换量联盟等。推广策略应该是先小规模投放,通过埋点或第三方统计工具对数据进行统计分析,分析哪些渠道的投放更加的优质(防刷,重点跟踪用户行为),价格更低,从而对该渠道进行精准投放,达到预期的效果。
    获取用户时应该注意的是该产品的落地页设计要合理,重点突出该产品的定位,因为很多用户浏览产品都是带有目的性和针对性的,简单来说就是用户可能存在着这种需求才会浏览该产品。
    在这里插入图片描述
    实际上在获取用户以后,还有一个重要的步骤就是激活用户,现在常用的用户激活方式就是用户在注册以后,将获得各种加息券、抵用券、红包等,以吸引用户来体验该产品。比如说注册了滴滴出行,送了一个免单券,碰巧今天下雨,用了滴滴出行,不仅不用花钱,司机还平安的把我送到家,这极大的提升了用户体验,在以后想要打车的时候,可能首先想到的就是滴滴出行,产品的差异化就体现出来了。长此以往,用户对该产品就产生了依赖心理,离开的成本也会变高。
    在滴滴出行,其实获取用户包含2个部分,一个是司机,一个是乘客,并且这两个是存在先后顺序的,即先要有司机,然后才会有用户。在产品初期,主要的推广方式还是地推试行,后面才会大规模的推广。首先,滴滴通过寻找司机,给司机相应的报酬和优惠,吸引司机来滴滴上注册,并且自己找托,让托去乘坐已注册滴滴出行司机的车,从而给司机形成意识,即这个东西还挺靠谱,通过手机就能接单,接送乘客,比开出租车方便多了,开出租车还得自己留意路上的行人,沿路找客人,另外再加上平台的补贴和奖励,司机会觉得这个东西真不错,每天下完班跑个滴滴,赚点外快,生活美滋滋啊。在有了一定量的司机之后,就会做用户推广,让用户去注册下单,并且给用户各种奖励,比如说首单免,发放一定额度的优惠券,吸引用户使用,由于价格优惠,服务到位,方便快捷,从而形成用户随时呼叫快车,司机随时接单的良性循环。

    5.2.2提高活跃度

    用户活跃度,不同的企业对其的定义都不太一样,不过大多数都可以用每次登录停留时长、日平均停留时长这一指标来进行衡量。提升用户活跃度,需要建立用户画像和用户标签,进行精细化运营,针对不同的用户采用不同的营销或推荐策略,说白了就是该产品能解决不用用户的不同需求。
    在滴滴出行APP上,用于提高用户活跃度和留存率的一个是会员和积分体系,一个是流量兑换中心。会员体系入口未免有点太小,要是不注意估计用户都看不见,另外关于积分(滴币),我都找不到自己的积分是多少,且最近几天积分商城一直都存在故障,虽然建立了会员和积分体系,个人感觉还是存在有一些问题的。其实真正点进去以后,我们可以发现滴滴在积分和会员体系中也是下了很大功夫的,但是你是不是隐藏的有点太深了,以至于用户都难以发现的你的存在,是不是应该将自己的良苦用心好好展示给用户看一下呢?另外一个流量兑换中心则根据会员等级限定可兑换的次数,关于这一点也许能刺激一下用户,但对于学生党来说,流量都无限了,估计都懒得去点,关于活动细则的提交流量申请该怎么提交?月末流量因系统升级不可兑换,在升级好之后是否能积攒到下个月继续兑换?是不是应该在流量兑换页面提供一个了解会员等级的入口?
    在这里插入图片描述在这里插入图片描述在这里插入图片描述
    以滴滴出行为例,提出以下几种改进方案:
    (1)针对车主,公司可以推荐一些有关汽车保养、美容、资讯方面的高质量软文,同时引入推荐策略,针对车主的喜好进一步推荐相关的文章;针对打车用户,可以推荐一些有关打车安全或相关注意事项、旅游出行方面的文章,解决用户的情感和安全需求来提高用户活跃度。
    (2)建立用户签到体系,即滴滴用户或车主在APP上签到,即可获取相应积分,或者也可以完成相应的任务来获取额外积分,在获取到积分以后去积分商城兑换商品、折扣券、优惠券等。
    (3)做个小程序,邀请好友一起来玩有奖励,比如说汽车知识小PK,和以前的微信最强大脑小程序有点类似,增加创意和可玩性,提高用户活跃度。
    (4)是不是可以在个人中心的头像位置显示会员勋章和所拥有的积分,或在和设置同等级的位置加入我的积分和我的会员等级标签(吸引用户眼球,既然煞费苦心做了,为什么不好好展现给用户呢?),并且在每次下单完成后提示用户离下一个等级还有多远,到了下一个等级赋予什么样的新特权(为满足用户的荣誉感),如“亲爱的*先生,您还差10公里就到达钻石会员了,届时将为您开启快速通道权限,最大限度缩短叫车时间,加油,看好你哦,具体详情请点击。。。。。。”
    提高用户活跃度,主要是得做能吸引用户的产品,增加创意,增加可玩性。

    5.2.3提高留存率

    所谓用户留存,即用户在注册使用该产品之后,在一段时间内还继续使用过该产品。留存率=登录用户数/新增用户数*100%。其中,新增用户数是当前时间段内新注册并登录应用的用户数,登录用户数是当前时间段内至少登录过一次的用户数。留存率反映的是一种转化率,由初期不稳定的用户转化为活跃用户、忠诚用户的过程。
    常用的几个留存率指标:次日留存、3日留存、7日留存、30日留存。
    提高用户留存的2个方案如下:
    (1)要想提高用户留存,就必须提高产品的差异化,即产品在某个方面做到了极致,或者说行业内最好,这时用户想解决某种需求的时候首先会想到的就是你的产品,那么留存率自然就上升了。比如说我想解决一个技术问题,首先想到的是用百度,当百度解决不了的时候就用谷歌(其实谷歌好,不过翻墙比较慢,延迟大,用户体验相对较差);想看一些运营方面的文章,首先想到的是人人都是产品经理,因为文章质量高啊;想打车的时候首先想到的就是滴滴,因为那个方便;当然,针对提高用户活跃度的方式也同样对提高用户留存率有效。
    (2)定义流失用户,即满足什么样的条件就判定为流失用户。对于流失用户采取Push、邮件或短信的方式召回,一般为了吸引用户再次使用,会发放一些优惠券、折扣券等。在对流失用户召回的同时,对自己的产品进行思考,分析用户流失的原因是什么,是用户体验太差、市面上出现了替代品、用户产生了疲劳感、整个行业发生了大规模变化、还是用户的需求已变,这些可以使用调查问卷或行业经验来进行分析。
    总而言之,一个产品有一个好的定位,并且通过不断迭代把这个定位做到极致,做到同行的No.1,不管是方便、高效、用户体验还是其他方面,这个时候你自然就能抓住用户。

    5.2.4获利

    一般来说,企业获取利润的方式有3种:付费产品、在产品内付费、广告付费。实际上,如果一个产品能做好前面的3点,并且在用户引流没什么太大问题的情况下,赚取利润就变成了水到渠成的一件事了。
    在此阶段,需要考虑两个指标值,一是ROI(投资回报率)= 销售额/获客成本,二是LTV(用户生命周期价值)= 销售额-获客成本。对于一个企业来讲,ROI和LTV越大越好,要想提高ROI和LTV,就需要提高销售额,降低获客成本。
    提高销售额:销售额=UV转化率平均客单价,因此要想提高销售额,必须通过推广或老用户传播提高浏览量,通过对各个环节的优化提高用户转化率,并且通过满赠、满减等活动促进用户提升客单价。
    降低获客成本:在前期,需要通过各种渠道对产品进行推广,以提高产品的曝光度和知名度,推广方式包括线上线下等。降低获客成本的最佳方式是先尝试小规模投放,后面通过数据分析对用户来源和质量进行分析,加大对优质渠道的精准投放。
    关于滴滴,现在的盈利方式大致有3种,一是从司机那抽取提成,二是通过保险商城,三是通过和联通、招商银行合作(毕竟开了流量入口)来赚取相关利润。
    在这里插入图片描述在这里插入图片描述在这里插入图片描述
    关于滴滴获取利润,提出2点建议:
    (1)滴滴出行会产生很多的轨迹数据,对这些轨迹数据和用户场景进行挖掘,寻求盈利。比如说通过对轨迹数据进行挖掘,发现使用滴滴的用户在某个商业大厦的人比较多,此时滴滴可以和附近的商家,比如说咖啡厅进行合作,首先通过滴滴送一个该商家的优惠券并提供商家位置,通过地图我找到了这家咖啡厅,喝了咖啡,感觉味道、服务、环境都不错,以后我就会经常来这,通过合作拿提成,可以作为滴滴的一个收入来源。
    (2)为了促进乘客和司机购买保险,在每次订单支付完成之后弄一个小抽奖环节,种类包含打车优惠券、抵扣券和购买保险的优惠券、抵扣券,主要重点还是送保险券吸引乘客和司机买保险,实现交叉营销

    5.2.5传播

    传播的方式主要有2种:一是产品体验好,用户自发传播推荐给身边的朋友,这种传播成本最低,同时用户质量也相对较高;二是通过邀请好友即可获取优惠券的方式刺激用户进行传播,可以采取三级分销体系和投资比例分成方式。
    提升产品体验,不单单是产品结构清晰、交互设计合理,最重要的一点是提高产品差异度,抓住用户,针对不同用户的不同需求提供不同的服务,这通常需要构建用户画像体系,很多小企业可能很难做到。并且提升产品体验,往往需要大量的A/B测试和产品迭代,有很长的路要走。
    邀请好友体验,可以说是最简单粗暴的一种方式了,具体流程是邀请到好友注册或下单(不同时期考虑的指标点可能有变),双方都可以获取到等额或不等的优惠券、折扣券或提成,这其实是一种分销、投资分成体系,为了避免传销嫌疑,需要对其设置上限。下面的滴滴出行APP邀请好友,好友享首单5元立减、自己得5元现金。
    在这里插入图片描述在这里插入图片描述

    6额外的想法

    现在的滴滴公益可以说是一个筹款平台,人人都可以奉献自己的爱心,帮助那些真正需要帮助的人,这也许是滴滴的初心。针对这一点简单提一下自己的想法:
    (1)是不是可以做成用户每支付完成一个订单,滴滴就向贫困地区捐献一分钱,并且以弹窗的形式提醒用户,以提高用户的参与度,激发用户的情感诉求,下次一想到打车我就会想起滴滴,因为我想为贫困地区的人们贡献一下自己的微薄之力。
    (2)加入一个滴滴公司参与公益的栏目,对品牌进行宣传和推广,提升品牌和公关形象,吸引更多的用户参与到公益事业中。
    在这里插入图片描述在这里插入图片描述

    7说明

    本文纯属用于交流,上面的所有意见、建议、问题和疑惑也均是个人拙见,可能有些已经开展实施,有些已经通过用户检验,也可能是本人资质愚钝,不能理解滴滴设计师的良苦用心,莫怪莫怪!!!!若是有什么问题疑惑,欢迎大家评论指教,谢谢。

    参考资料

    [1]愚人节套路?来一场脑洞策划:以滴滴打车为例 吴扯扯 原创发布于人人都是产品经理
    [2]聊聊用户成长体系:浅析「KEEP」的用户成长体系 有馅儿的丸子 原创发布于人人都是产品经理
    [3]关于用户成长体系,一份不能错过的笔记 豆丁 原创发布于人人都是产品经理
    [4] http://www.lianxianjia.com/ecnr/194939.html
    [5] 360产品经理:我是如何用增长黑客思维,13天收获2,000+高质量简历 金师兴 原创发布于人人都是产品经理
    [6]增长奥秘:滴滴当年如何挖掘最有效的渠道和增长方法(以滴滴小巴为例)?李森 原创发布于鸟哥笔记
    [7] 增长逻辑,0到1的锐变 李森 原创发布于鸟哥笔记
    [8] 滴滴出行高级运营:快速低成本用户增长策略 李森 原创发布于鸟哥笔记
    [9] AARRR模型案例:利用数据优化渠道投放,并实现用户增长 殷为业 原创发布于人人都是产品经理
    [10] 触动人心的运营策略02:互金用户生命周期管理的完整方法论 道是无 发布于人人都是产品经理

    展开全文
  • 业务逻辑层是专门处理软件业务需求的一层,处于数据库之上,服务层之下,完成一些列对Domain Object的CRUD,作为一组微服务提供给服务层来组织在暴露给表现层,如库存检查,用法合法性检查,订单创建。  业务逻辑...

    转载地址:http://www.cnblogs.com/whitewolf/archive/2012/05/29/2524881.html

    业务逻辑层是专门处理软件业务需求的一层,处于数据库之上,服务层之下,完成一些列对Domain Object的CRUD,作为一组微服务提供给服务层来组织在暴露给表现层,如库存检查,用法合法性检查,订单创建。

       业务逻辑层包含领域对象模型,领域实体,业务规则,验证规则,业务流程。1:领域对象模型为系统结构描述,包含实体功能描述,实体之间的关系。领域模型处于天生的复杂性:2:领域实体:业务层是一些操作业务对象(BO)的处理。业务对象包含数据和行为,是一个完整的业务对象。其不同于上节架构设计中服务层的简单理解提到的数据迁移对象(dto),对于dto存在数据的,不存在行为,dto是bo(ddd中又称do)的子集,负责与特定界面需求的扁平化实体,dto仅仅是一个数据载体,需要跨越应用程序边界,而业务对象则不会存在复制迁移,往往一个业务对象存在一个或者多个数据迁移对象。3:业务最大的逻辑就在处理一些列现实世界的规则,这也是软件中最容易变化的部分,这里通常会出现我们众多的if-else或者switch-case的地方。也这因为如果说以个人觉得在我们的项目最应该关系和分离需求的层次。4:验证规则:业务规则很大程度上也是对对象的数据验证,验证业务对象的当前数据状态。我觉得在每个业务对象上都应该存在一个对外部对象暴露的验证接口,可以考虑微软企业库的VAB 基于Attribute声明式验证或者上节流畅的验证组件:FluentValidation中的FluentValidation验证组件基于IOC的解耦。

       业务层模式:在常见的业务层模式中主要分为过程是模式和面向对象模式。过程模式有是事务性脚本和表模式,而面向对象模式为活动记录模式和领域驱动模式。理论上说事务性脚本模式是最简单的开发模式,其前期投入下,但随着项目周期和复杂度上升明显,而领域模型(DDD)前期投入较大,但是理论上说是随着项目周期和复杂度呈线性增加,当然这些都是理论值。

      1:事务脚本模式是业务逻辑层最简单的模式,面向过程模式。该模式以用于的操作为起点,设计业务组件,即业务逻辑直接映射到用户界面的操作。这通常是从表现层逻辑出发,表现层我需要什么业务层提供什么,直到数据层。针对没一个用户的新功能都需要新增一个从UI到关系数据库的分支流程。其使用与逻辑不是很复杂或者变化不大稳定的应用系统开发。其不需要付出与业务无关的额外代价,并且在现代VS之类的IDE帮助下能够很快的进行快速应用开发(RAD)。也由于这种优势,也是其最大的劣势,程序中充满了IF-else,switch-case之类的逻辑或者大量的static的方法,每个功能都是一个程序分支,这对代码无法重用。编码不易于维护,对复杂项目和变化需求不适应。

      2:表模式:为每个数据库表定义一个表模块类,包含操作该数据的所有行为方法。作为一个容器,将数据和行为组织在一起。其对数据的粒度针对于数据表,而非数据行,因此需要以集合或者表传递数据信息。表模式基于对象但是完全又数据库驱动开发,在业务模型和数据库关系模型显著差异的情况下,应对需求,并不是那么适合。但是在.net中提供的一些列如强类型DataSet等IDE的辅助下自动生成大量的代码,也是一个不错的选择,因为部分数据库的操作趋于自动化。表模式没太过于关注业务,而是关注数据库表结构。而业务逻辑和领域问题才是软件核心。

      3:活动记录模式:一个以数据库表一行Row为对象,并且对象中包含行为和数据的模式方法。其数据对象很大程度的接近数据库表结构。在活动记录模式对象中通常也包含操作对象的CRUD行为,数据验证等业务规则。对于业务不是很复杂,对象关系与关系模型映射不具有很大差异情况,活动记录模式会运用的很好。活动模式比较简单化设计,在上现行的很多如Linq to sql,ActiveRecord框架的辅助下,将针对问题领域不是太过复杂的项目十分有用。但是其模式和数据库表结构的相互依赖,导致若你修改数据库结构,你不得不同时修改对象以及相关逻辑。如果不能保证数据库关系模型和对象模式的很大程度的相似这就进入的困境。

    4:领域模型:在前面的几种模式都是项目开始站在了以数据为中心的角度,而不是业务本身的问题领域。而领域模型关注系统问题领域,首先开始为领域对象设计。与活动记录模式来说,领域模型完全站在了问题领域业务概念模型一边,与数据库,持久化完成独立,其推崇持久化透明(POCO)。其可以充分利用面向对象设计,不受持久化机制的任何约束。其实完全又业务驱动出来的。但是其最大的优势如上各个模式一样也是其最大的劣势对象模型和关系模型具有天然的阻抗,我们的领域实体早晚需要映射到持久化机制。还好的是当前有NHibearnate,EF,Fluent NHibearnate这类ORM框架辅助。在DDD中包含UOW,仓储,值类型和聚合根,领域事件,领域跟踪一类的概念,这将在以后具体说明。

      模式的选择在与架构师的决定,这也是架构师具有挑战意义的职责,需要根据具体的项目需求,团队,个人等外界因素最终决定,不存在万能的模式,也不存在完美的设计。


    作者:破  狼 
    出处:http://www.cnblogs.com/whitewolf/ 
    本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。该文章也同时发布在我的独立博客中-博客园--破狼51CTO--破狼


    展开全文
  • 另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/44996879


    在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。


    另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把ListView的源码通读了一遍,基本了解了它的工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏,因为ListView的代码量比较大,复杂度也很高,很难用文字表达清楚,于是我就放弃了把它写成一篇博客的想法。那么现在回想起来这件事我已经肠子都悔青了,因为没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。于是现在我又重新定下心来再次把ListView的源码重读了一遍,那么这次我一定要把它写成一篇博客,分享给大家的同时也当成我自己的笔记吧。


    首先我们先来看一下ListView的继承结构,如下图所示:




    可以看到,ListView的继承结构还是相当复杂的,它是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,因此我们从这一点就可以猜出来,ListView和GridView在工作原理和实现上都是有很多共同点的。然后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup,后面就是我们所熟知的了。先把ListView的继承结构了解一下,待会儿有助于我们更加清晰地分析代码。


    Adapter的作用


    Adapter相信大家都不会陌生,我们平时使用ListView的时候一定都会用到它。那么话说回来大家有没有仔细想过,为什么需要Adapter这个东西呢?总感觉正因为有了Adapter,ListView的使用变得要比其它控件复杂得多。那么这里我们就先来学习一下Adapter到底起到了什么样的一个作用。


    其实说到底,控件就是为了交互和展示数据用的,只不过ListView更加特殊,它是为了展示很多很多数据用的,但是ListView只承担交互和展示工作而已,至于这些数据来自哪里,ListView是不关心的。因此,我们能设想到的最基本的ListView工作模式就是要有一个ListView控件和一个数据源。


    不过如果真的让ListView和数据源直接打交道的话,那ListView所要做的适配工作就非常繁杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据而已,至于这个数据源到底是什么样类型,并没有严格的定义,有可能是数组,也有可能是集合,甚至有可能是数据库表中查询出来的游标。所以说如果ListView真的去为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它本身应该负责的工作范围,不再是仅仅承担交互和展示工作就可以了,这样ListView就会变得比较臃肿。


    那么显然Android开发团队是不会允许这种事情发生的,于是就有了Adapter这样一个机制的出现。顾名思义,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示:




    当然Adapter的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我们在Adapter当中去重写,就是getView()方法,这个在下面的文章中还会详细讲到。


    RecycleBin机制


    那么在开始分析ListView的源码之前,还有一个东西是我们提前需要了解的,就是RecycleBin机制,这个机制也是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。其实RecycleBin的代码并不多,只有300行左右,它是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。那我们来看一下RecycleBin中的主要代码,如下所示:

    /**
     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
     * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
     * those views which were onscreen at the start of a layout. By
     * construction, they are displaying current information. At the end of
     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
     * are old views that could potentially be used by the adapter to avoid
     * allocating views unnecessarily.
     * 
     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
     * @see android.widget.AbsListView.RecyclerListener
     */
    class RecycleBin {
    	private RecyclerListener mRecyclerListener;
    
    	/**
    	 * The position of the first view stored in mActiveViews.
    	 */
    	private int mFirstActivePosition;
    
    	/**
    	 * Views that were on screen at the start of layout. This array is
    	 * populated at the start of layout, and at the end of layout all view
    	 * in mActiveViews are moved to mScrapViews. Views in mActiveViews
    	 * represent a contiguous range of Views, with position of the first
    	 * view store in mFirstActivePosition.
    	 */
    	private View[] mActiveViews = new View[0];
    
    	/**
    	 * Unsorted views that can be used by the adapter as a convert view.
    	 */
    	private ArrayList<View>[] mScrapViews;
    
    	private int mViewTypeCount;
    
    	private ArrayList<View> mCurrentScrap;
    
    	/**
    	 * Fill ActiveViews with all of the children of the AbsListView.
    	 * 
    	 * @param childCount
    	 *            The minimum number of views mActiveViews should hold
    	 * @param firstActivePosition
    	 *            The position of the first view that will be stored in
    	 *            mActiveViews
    	 */
    	void fillActiveViews(int childCount, int firstActivePosition) {
    		if (mActiveViews.length < childCount) {
    			mActiveViews = new View[childCount];
    		}
    		mFirstActivePosition = firstActivePosition;
    		final View[] activeViews = mActiveViews;
    		for (int i = 0; i < childCount; i++) {
    			View child = getChildAt(i);
    			AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
    			// Don't put header or footer views into the scrap heap
    			if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
    				// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
    				// active views.
    				// However, we will NOT place them into scrap views.
    				activeViews[i] = child;
    			}
    		}
    	}
    
    	/**
    	 * Get the view corresponding to the specified position. The view will
    	 * be removed from mActiveViews if it is found.
    	 * 
    	 * @param position
    	 *            The position to look up in mActiveViews
    	 * @return The view if it is found, null otherwise
    	 */
    	View getActiveView(int position) {
    		int index = position - mFirstActivePosition;
    		final View[] activeViews = mActiveViews;
    		if (index >= 0 && index < activeViews.length) {
    			final View match = activeViews[index];
    			activeViews[index] = null;
    			return match;
    		}
    		return null;
    	}
    
    	/**
    	 * Put a view into the ScapViews list. These views are unordered.
    	 * 
    	 * @param scrap
    	 *            The view to add
    	 */
    	void addScrapView(View scrap) {
    		AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    		if (lp == null) {
    			return;
    		}
    		// Don't put header or footer views or views that should be ignored
    		// into the scrap heap
    		int viewType = lp.viewType;
    		if (!shouldRecycleViewType(viewType)) {
    			if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
    				removeDetachedView(scrap, false);
    			}
    			return;
    		}
    		if (mViewTypeCount == 1) {
    			dispatchFinishTemporaryDetach(scrap);
    			mCurrentScrap.add(scrap);
    		} else {
    			dispatchFinishTemporaryDetach(scrap);
    			mScrapViews[viewType].add(scrap);
    		}
    
    		if (mRecyclerListener != null) {
    			mRecyclerListener.onMovedToScrapHeap(scrap);
    		}
    	}
    
    	/**
    	 * @return A view from the ScrapViews collection. These are unordered.
    	 */
    	View getScrapView(int position) {
    		ArrayList<View> scrapViews;
    		if (mViewTypeCount == 1) {
    			scrapViews = mCurrentScrap;
    			int size = scrapViews.size();
    			if (size > 0) {
    				return scrapViews.remove(size - 1);
    			} else {
    				return null;
    			}
    		} else {
    			int whichScrap = mAdapter.getItemViewType(position);
    			if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
    				scrapViews = mScrapViews[whichScrap];
    				int size = scrapViews.size();
    				if (size > 0) {
    					return scrapViews.remove(size - 1);
    				}
    			}
    		}
    		return null;
    	}
    
    	public void setViewTypeCount(int viewTypeCount) {
    		if (viewTypeCount < 1) {
    			throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
    		}
    		// noinspection unchecked
    		ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
    		for (int i = 0; i < viewTypeCount; i++) {
    			scrapViews[i] = new ArrayList<View>();
    		}
    		mViewTypeCount = viewTypeCount;
    		mCurrentScrap = scrapViews[0];
    		mScrapViews = scrapViews;
    	}
    
    }

    这里的RecycleBin代码并不全,我只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析ListView的工作原理将会有很大的帮助。

    • fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
    • getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
    • addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
    • getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
    • setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。


    了解了RecycleBin中的主要方法以及它们的用处之后,下面就可以开始来分析ListView的工作原理了,这里我将还是按照以前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,不然的话要是把ListView所有的代码都贴出来,那么本篇文章将会很长很长了。


    第一次Layout


    不管怎么说,ListView即使再特殊最终还是继承自View的,因此它的执行流程还将会按照View的规则来执行,对于这方面不太熟悉的朋友可以参考我之前写的 Android视图绘制流程完全解析,带你一步步深入了解View(二) 。


    View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了,因此我们本篇文章也是主要分析的这个方法里的内容。


    如果你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView中实现的,代码如下所示:

    /**
     * Subclasses should NOT override this method but {@link #layoutChildren()}
     * instead.
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    	super.onLayout(changed, l, t, r, b);
    	mInLayout = true;
    	if (changed) {
    		int childCount = getChildCount();
    		for (int i = 0; i < childCount; i++) {
    			getChildAt(i).forceLayout();
    		}
    		mRecycler.markChildrenDirty();
    	}
    	layoutChildren();
    	mInLayout = false;
    }
    可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码如下所示:
    @Override
    protected void layoutChildren() {
        final boolean blockLayoutRequests = mBlockLayoutRequests;
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = true;
        } else {
            return;
        }
        try {
            super.layoutChildren();
            invalidate();
            if (mAdapter == null) {
                resetList();
                invokeOnItemScrollListener();
                return;
            }
            int childrenTop = mListPadding.top;
            int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
            int childCount = getChildCount();
            int index = 0;
            int delta = 0;
            View sel;
            View oldSel = null;
            View oldFirst = null;
            View newSel = null;
            View focusLayoutRestoreView = null;
            // Remember stuff we will need down below
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                index = mNextSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    newSel = getChildAt(index);
                }
                break;
            case LAYOUT_FORCE_TOP:
            case LAYOUT_FORCE_BOTTOM:
            case LAYOUT_SPECIFIC:
            case LAYOUT_SYNC:
                break;
            case LAYOUT_MOVE_SELECTION:
            default:
                // Remember the previously selected view
                index = mSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    oldSel = getChildAt(index);
                }
                // Remember the previous first child
                oldFirst = getChildAt(0);
                if (mNextSelectedPosition >= 0) {
                    delta = mNextSelectedPosition - mSelectedPosition;
                }
                // Caution: newSel might be null
                newSel = getChildAt(index + delta);
            }
            boolean dataChanged = mDataChanged;
            if (dataChanged) {
                handleDataChanged();
            }
            // Handle the empty set by removing all views that are visible
            // and calling it a day
            if (mItemCount == 0) {
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new IllegalStateException("The content of the adapter has changed but "
                        + "ListView did not receive a notification. Make sure the content of "
                        + "your adapter is not modified from a background thread, but only "
                        + "from the UI thread. [in ListView(" + getId() + ", " + getClass() 
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }
            setSelectedPositionInt(mNextSelectedPosition);
            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            // reset the focus restoration
            View focusLayoutRestoreDirectChild = null;
            // Don't put header or footer views into the Recycler. Those are
            // already cached in mHeaderViews;
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i));
                    if (ViewDebug.TRACE_RECYCLER) {
                        ViewDebug.trace(getChildAt(i),
                                ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                    }
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }
            // take focus back to us temporarily to avoid the eventual
            // call to clear focus when removing the focused child below
            // from messing things up when ViewRoot assigns focus back
            // to someone else
            final View focusedChild = getFocusedChild();
            if (focusedChild != null) {
                // TODO: in some cases focusedChild.getParent() == null
                // we can remember the focused view to restore after relayout if the
                // data hasn't changed, or if the focused position is a header or footer
                if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
                    focusLayoutRestoreDirectChild = focusedChild;
                    // remember the specific view that had focus
                    focusLayoutRestoreView = findFocus();
                    if (focusLayoutRestoreView != null) {
                        // tell it we are going to mess with it
                        focusLayoutRestoreView.onStartTemporaryDetach();
                    }
                }
                requestFocus();
            }
            // Clear out old views
            detachAllViewsFromParent();
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                if (childCount == 0) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(0, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - 1, false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - 1, childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) {
                        sel = fillSpecific(mFirstPosition,
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }
            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();
            if (sel != null) {
                // the current selected item should get focus if items
                // are focusable
                if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
                    final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
                            focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
                    if (!focusWasTaken) {
                        // selected item didn't take focus, fine, but still want
                        // to make sure something else outside of the selected view
                        // has focus
                        final View focused = getFocusedChild();
                        if (focused != null) {
                            focused.clearFocus();
                        }
                        positionSelector(sel);
                    } else {
                        sel.setSelected(false);
                        mSelectorRect.setEmpty();
                    }
                } else {
                    positionSelector(sel);
                }
                mSelectedTop = sel.getTop();
            } else {
                if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
                    View child = getChildAt(mMotionPosition - mFirstPosition);
                    if (child != null) positionSelector(child);
                } else {
                    mSelectedTop = 0;
                    mSelectorRect.setEmpty();
                }
                // even if there is not selected position, we may need to restore
                // focus (i.e. something focusable in touch mode)
                if (hasFocus() && focusLayoutRestoreView != null) {
                    focusLayoutRestoreView.requestFocus();
                }
            }
            // tell focus view we are done mucking with it, if it is still in
            // our view hierarchy.
            if (focusLayoutRestoreView != null
                    && focusLayoutRestoreView.getWindowToken() != null) {
                focusLayoutRestoreView.onFinishTemporaryDetach();
            }
            mLayoutMode = LAYOUT_NORMAL;
            mDataChanged = false;
            mNeedSync = false;
            setNextSelectedPositionInt(mSelectedPosition);
            updateScrollIndicators();
            if (mItemCount > 0) {
                checkSelectionChanged();
            }
            invokeOnItemScrollListener();
        } finally {
            if (!blockLayoutRequests) {
                mBlockLayoutRequests = false;
            }
        }
    }

    这段代码比较长,我们挑重点的看。首先可以确定的是,ListView当中目前还没有任何子View,数据都还是由Adapter管理的,并没有展示到界面上,因此第19行getChildCount()方法得到的值肯定是0。接着在第81行会根据dataChanged这个布尔型的值来判断执行逻辑,dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入到第90行的执行逻辑,调用RecycleBin的fillActiveViews()方法。按理来说,调用fillActiveViews()方法是为了将ListView的子View进行缓存的,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。


    接下来在第114行会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到第140行的default语句当中。而下面又会紧接着进行两次if判断,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到第145行的fillFromTop()方法,我们跟进去瞧一瞧:

    /**
     * Fills the list from top to bottom, starting with mFirstPosition
     *
     * @param nextTop The location where the top of the first item should be
     *        drawn
     *
     * @return The view that is currently selected
     */
    private View fillFromTop(int nextTop) {
        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
        if (mFirstPosition < 0) {
            mFirstPosition = 0;
        }
        return fillDown(mFirstPosition, nextTop);
    }
    从这个方法的注释中可以看出,它所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法本身并没有什么逻辑,就是判断了一下mFirstPosition值的合法性,然后调用fillDown()方法,那么我们就有理由可以猜测,填充ListView的操作是在fillDown()方法中完成的。进入fillDown()方法,代码如下所示:
    /**
     * Fills the list from pos down to the end of the list view.
     *
     * @param pos The first position to put in the list
     *
     * @param nextTop The location where the top of the item associated with pos
     *        should be drawn
     *
     * @return The view that is currently selected, if it happens to be in the
     *         range that we draw.
     */
    private View fillDown(int pos, int nextTop) {
        View selectedView = null;
        int end = (getBottom() - getTop()) - mListPadding.bottom;
        while (nextTop < end && pos < mItemCount) {
            // is this the selected item?
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
            nextTop = child.getBottom() + mDividerHeight;
            if (selected) {
                selectedView = child;
            }
            pos++;
        }
        return selectedView;
    }

    可以看到,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。


    那么while循环当中又做了什么事情呢?值得让人留意的就是第18行调用的makeAndAddView()方法,进入到这个方法当中,代码如下所示:

    /**
     * Obtain the view and add it to our list of children. The view can be made
     * fresh, converted from an unused view, or used as is if it was in the
     * recycle bin.
     *
     * @param position Logical position in the list
     * @param y Top or bottom edge of the view to add
     * @param flow If flow is true, align top edge to y. If false, align bottom
     *        edge to y.
     * @param childrenLeft Left edge where children should be positioned
     * @param selected Is this position selected?
     * @return View that was added
     */
    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            // Try to use an exsiting view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }
        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }
    这里在第19行尝试从RecycleBin当中快速获取一个active view,不过很遗憾的是目前RecycleBin当中还没有缓存任何的View,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行,到第28行会调用obtainView()方法来再次尝试获取一个View,这次的obtainView()方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中。那么obtainView()内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:
    /**
     * Get a view and have it show the data associated with the specified
     * position. This is called when we have already discovered that the view is
     * not available for reuse in the recycle bin. The only choices left are
     * converting an old view or making a new one.
     * 
     * @param position
     *            The position to display
     * @param isScrap
     *            Array of at least 1 boolean, the first entry will become true
     *            if the returned view was taken from the scrap heap, false if
     *            otherwise.
     * 
     * @return A view displaying the data associated with the specified position
     */
    View obtainView(int position, boolean[] isScrap) {
    	isScrap[0] = false;
    	View scrapView;
    	scrapView = mRecycler.getScrapView(position);
    	View child;
    	if (scrapView != null) {
    		child = mAdapter.getView(position, scrapView, this);
    		if (child != scrapView) {
    			mRecycler.addScrapView(scrapView);
    			if (mCacheColorHint != 0) {
    				child.setDrawingCacheBackgroundColor(mCacheColorHint);
    			}
    		} else {
    			isScrap[0] = true;
    			dispatchFinishTemporaryDetach(child);
    		}
    	} else {
    		child = mAdapter.getView(position, null, this);
    		if (mCacheColorHint != 0) {
    			child.setDrawingCacheBackgroundColor(mCacheColorHint);
    		}
    	}
    	return child;
    }
    obtainView()方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,同样的道理,这里肯定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?当然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。


    那么我们平时写ListView的Adapter时,getView()方法通常会怎么写呢?这里我举个简单的例子:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
    	Fruit fruit = getItem(position);
    	View view;
    	if (convertView == null) {
    		view = LayoutInflater.from(getContext()).inflate(resourceId, null);
    	} else {
    		view = convertView;
    	}
    	ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
    	TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
    	fruitImage.setImageResource(fruit.getImageId());
    	fruitName.setText(fruit.getName());
    	return view;
    }
    getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。


    那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:

    /**
     * Add a view as a child and make sure it is measured (if necessary) and
     * positioned properly.
     *
     * @param child The view to add
     * @param position The position of this child
     * @param y The y position relative to which this view will be positioned
     * @param flowDown If true, align top edge to y. If false, align bottom
     *        edge to y.
     * @param childrenLeft Left edge where children should be positioned
     * @param selected Is this position selected?
     * @param recycled Has this view been pulled from the recycle bin? If so it
     *        does not need to be remeasured.
     */
    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean recycled) {
        final boolean isSelected = selected && shouldShowSelector();
        final boolean updateChildSelected = isSelected != child.isSelected();
        final int mode = mTouchMode;
        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
                mMotionPosition == position;
        final boolean updateChildPressed = isPressed != child.isPressed();
        final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
        // Respect layout params that are already in the view. Otherwise make some up...
        // noinspection unchecked
        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
        if (p == null) {
            p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT, 0);
        }
        p.viewType = mAdapter.getItemViewType(position);
        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
                p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p);
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
        }
        if (updateChildSelected) {
            child.setSelected(isSelected);
        }
        if (updateChildPressed) {
            child.setPressed(isPressed);
        }
        if (needToMeasure) {
            int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                    mListPadding.left + mListPadding.right, p.width);
            int lpHeight = p.height;
            int childHeightSpec;
            if (lpHeight > 0) {
                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
            } else {
                childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            }
            child.measure(childWidthSpec, childHeightSpec);
        } else {
            cleanupLayoutState(child);
        }
        final int w = child.getMeasuredWidth();
        final int h = child.getMeasuredHeight();
        final int childTop = flowDown ? y : y - h;
        if (needToMeasure) {
            final int childRight = childrenLeft + w;
            final int childBottom = childTop + h;
            child.layout(childrenLeft, childTop, childRight, childBottom);
        } else {
            child.offsetLeftAndRight(childrenLeft - child.getLeft());
            child.offsetTopAndBottom(childTop - child.getTop());
        }
        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
            child.setDrawingCacheEnabled(true);
        }
    }
    setupChild()方法当中的代码虽然比较多,但是我们只看核心代码的话就非常简单了,刚才调用obtainView()方法获取到的子元素View,这里在第40行调用了addViewInLayout()方法将它添加到了ListView当中。那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上。


    那么到此为止,第一次Layout过程结束。


    第二次Layout


    虽然我在源码中并没有找出具体的原因,但如果你自己做一下实验的话就会发现,即使是一个再简单的View,在展示到界面上之前都会经历至少两次onMeasure()和两次onLayout()的过程。其实这只是一个很小的细节,平时对我们影响并不大,因为不管是onMeasure()或者onLayout()几次,反正都是执行的相同的逻辑,我们并不需要进行过多关心。但是在ListView中情况就不一样了,因为这就意味着layoutChildren()过程会执行两次,而这个过程当中涉及到向ListView中添加子元素,如果相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。因此ListView在layoutChildren()过程当中做了第二次Layout的逻辑处理,非常巧妙地解决了这个问题,下面我们就来分析一下第二次Layout的过程。


    其实第二次Layout和第一次Layout的基本流程是差不多的,那么我们还是从layoutChildren()方法开始看起:

    @Override
    protected void layoutChildren() {
        final boolean blockLayoutRequests = mBlockLayoutRequests;
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = true;
        } else {
            return;
        }
        try {
            super.layoutChildren();
            invalidate();
            if (mAdapter == null) {
                resetList();
                invokeOnItemScrollListener();
                return;
            }
            int childrenTop = mListPadding.top;
            int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
            int childCount = getChildCount();
            int index = 0;
            int delta = 0;
            View sel;
            View oldSel = null;
            View oldFirst = null;
            View newSel = null;
            View focusLayoutRestoreView = null;
            // Remember stuff we will need down below
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                index = mNextSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    newSel = getChildAt(index);
                }
                break;
            case LAYOUT_FORCE_TOP:
            case LAYOUT_FORCE_BOTTOM:
            case LAYOUT_SPECIFIC:
            case LAYOUT_SYNC:
                break;
            case LAYOUT_MOVE_SELECTION:
            default:
                // Remember the previously selected view
                index = mSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    oldSel = getChildAt(index);
                }
                // Remember the previous first child
                oldFirst = getChildAt(0);
                if (mNextSelectedPosition >= 0) {
                    delta = mNextSelectedPosition - mSelectedPosition;
                }
                // Caution: newSel might be null
                newSel = getChildAt(index + delta);
            }
            boolean dataChanged = mDataChanged;
            if (dataChanged) {
                handleDataChanged();
            }
            // Handle the empty set by removing all views that are visible
            // and calling it a day
            if (mItemCount == 0) {
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new IllegalStateException("The content of the adapter has changed but "
                        + "ListView did not receive a notification. Make sure the content of "
                        + "your adapter is not modified from a background thread, but only "
                        + "from the UI thread. [in ListView(" + getId() + ", " + getClass() 
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }
            setSelectedPositionInt(mNextSelectedPosition);
            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            // reset the focus restoration
            View focusLayoutRestoreDirectChild = null;
            // Don't put header or footer views into the Recycler. Those are
            // already cached in mHeaderViews;
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i));
                    if (ViewDebug.TRACE_RECYCLER) {
                        ViewDebug.trace(getChildAt(i),
                                ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                    }
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }
            // take focus back to us temporarily to avoid the eventual
            // call to clear focus when removing the focused child below
            // from messing things up when ViewRoot assigns focus back
            // to someone else
            final View focusedChild = getFocusedChild();
            if (focusedChild != null) {
                // TODO: in some cases focusedChild.getParent() == null
                // we can remember the focused view to restore after relayout if the
                // data hasn't changed, or if the focused position is a header or footer
                if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
                    focusLayoutRestoreDirectChild = focusedChild;
                    // remember the specific view that had focus
                    focusLayoutRestoreView = findFocus();
                    if (focusLayoutRestoreView != null) {
                        // tell it we are going to mess with it
                        focusLayoutRestoreView.onStartTemporaryDetach();
                    }
                }
                requestFocus();
            }
            // Clear out old views
            detachAllViewsFromParent();
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                if (childCount == 0) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(0, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - 1, false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - 1, childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) {
                        sel = fillSpecific(mFirstPosition,
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }
            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();
            if (sel != null) {
                // the current selected item should get focus if items
                // are focusable
                if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
                    final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
                            focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
                    if (!focusWasTaken) {
                        // selected item didn't take focus, fine, but still want
                        // to make sure something else outside of the selected view
                        // has focus
                        final View focused = getFocusedChild();
                        if (focused != null) {
                            focused.clearFocus();
                        }
                        positionSelector(sel);
                    } else {
                        sel.setSelected(false);
                        mSelectorRect.setEmpty();
                    }
                } else {
                    positionSelector(sel);
                }
                mSelectedTop = sel.getTop();
            } else {
                if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
                    View child = getChildAt(mMotionPosition - mFirstPosition);
                    if (child != null) positionSelector(child);
                } else {
                    mSelectedTop = 0;
                    mSelectorRect.setEmpty();
                }
                // even if there is not selected position, we may need to restore
                // focus (i.e. something focusable in touch mode)
                if (hasFocus() && focusLayoutRestoreView != null) {
                    focusLayoutRestoreView.requestFocus();
                }
            }
            // tell focus view we are done mucking with it, if it is still in
            // our view hierarchy.
            if (focusLayoutRestoreView != null
                    && focusLayoutRestoreView.getWindowToken() != null) {
                focusLayoutRestoreView.onFinishTemporaryDetach();
            }
            mLayoutMode = LAYOUT_NORMAL;
            mDataChanged = false;
            mNeedSync = false;
            setNextSelectedPositionInt(mSelectedPosition);
            updateScrollIndicators();
            if (mItemCount > 0) {
                checkSelectionChanged();
            }
            invokeOnItemScrollListener();
        } finally {
            if (!blockLayoutRequests) {
                mBlockLayoutRequests = false;
            }
        }
    }

    同样还是在第19行,调用getChildCount()方法来获取子View的数量,只不过现在得到的值不会再是0了,而是ListView中一屏可以显示的子View数量,因为我们刚刚在第一次Layout过程当中向ListView添加了这么多的子View。下面在第90行调用了RecycleBin的fillActiveViews()方法,这次效果可就不一样了,因为目前ListView中已经有子View了,这样所有的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。


    接下来将会是非常非常重要的一个操作,在第113行调用了detachAllViewsFromParent()方法。这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。


    那么我们接着看,在第141行的判断逻辑当中,由于不再等于0了,因此会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断通常是成立的,因为mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,代码如下所示:

    /**
     * Put a specific item at a specific location on the screen and then build
     * up and down from there.
     *
     * @param position The reference view to use as the starting point
     * @param top Pixel offset from the top of this view to the top of the
     *        reference view.
     *
     * @return The selected view, or null if the selected view is outside the
     *         visible area.
     */
    private View fillSpecific(int position, int top) {
        boolean tempIsSelected = position == mSelectedPosition;
        View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
        // Possibly changed again in fillUp if we add rows above this one.
        mFirstPosition = position;
        View above;
        View below;
        final int dividerHeight = mDividerHeight;
        if (!mStackFromBottom) {
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            // This will correct for the top of the first view not touching the top of the list
            adjustViewsUpOrDown();
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                correctTooHigh(childCount);
            }
        } else {
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            // This will correct for the bottom of the last view not touching the bottom of the list
            adjustViewsUpOrDown();
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                 correctTooLow(childCount);
            }
        }
        if (tempIsSelected) {
            return temp;
        } else if (above != null) {
            return above;
        } else {
            return below;
        }
    }

    fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:

    /**
     * Obtain the view and add it to our list of children. The view can be made
     * fresh, converted from an unused view, or used as is if it was in the
     * recycle bin.
     *
     * @param position Logical position in the list
     * @param y Top or bottom edge of the view to add
     * @param flow If flow is true, align top edge to y. If false, align bottom
     *        edge to y.
     * @param childrenLeft Left edge where children should be positioned
     * @param selected Is this position selected?
     * @return View that was added
     */
    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            // Try to use an exsiting view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }
        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }

    仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。


    注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:

    /**
     * Add a view as a child and make sure it is measured (if necessary) and
     * positioned properly.
     *
     * @param child The view to add
     * @param position The position of this child
     * @param y The y position relative to which this view will be positioned
     * @param flowDown If true, align top edge to y. If false, align bottom
     *        edge to y.
     * @param childrenLeft Left edge where children should be positioned
     * @param selected Is this position selected?
     * @param recycled Has this view been pulled from the recycle bin? If so it
     *        does not need to be remeasured.
     */
    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean recycled) {
        final boolean isSelected = selected && shouldShowSelector();
        final boolean updateChildSelected = isSelected != child.isSelected();
        final int mode = mTouchMode;
        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
                mMotionPosition == position;
        final boolean updateChildPressed = isPressed != child.isPressed();
        final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
        // Respect layout params that are already in the view. Otherwise make some up...
        // noinspection unchecked
        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
        if (p == null) {
            p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT, 0);
        }
        p.viewType = mAdapter.getItemViewType(position);
        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
                p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p);
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
        }
        if (updateChildSelected) {
            child.setSelected(isSelected);
        }
        if (updateChildPressed) {
            child.setPressed(isPressed);
        }
        if (needToMeasure) {
            int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                    mListPadding.left + mListPadding.right, p.width);
            int lpHeight = p.height;
            int childHeightSpec;
            if (lpHeight > 0) {
                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
            } else {
                childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            }
            child.measure(childWidthSpec, childHeightSpec);
        } else {
            cleanupLayoutState(child);
        }
        final int w = child.getMeasuredWidth();
        final int h = child.getMeasuredHeight();
        final int childTop = flowDown ? y : y - h;
        if (needToMeasure) {
            final int childRight = childrenLeft + w;
            final int childBottom = childTop + h;
            child.layout(childrenLeft, childTop, childRight, childBottom);
        } else {
            child.offsetLeftAndRight(childrenLeft - child.getLeft());
            child.offsetTopAndBottom(childTop - child.getTop());
        }
        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
            child.setDrawingCacheEnabled(true);
        }
    }
    可以看到,setupChild()方法的最后一个参数是recycled,然后在第32行会对这个变量进行判断,由于recycled现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。


    经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。


    滑动加载更多数据


    经历了两次Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。


    由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码就肯定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
    	if (!isEnabled()) {
    		// A disabled view that is clickable still consumes the touch
    		// events, it just doesn't respond to them.
    		return isClickable() || isLongClickable();
    	}
    	final int action = ev.getAction();
    	View v;
    	int deltaY;
    	if (mVelocityTracker == null) {
    		mVelocityTracker = VelocityTracker.obtain();
    	}
    	mVelocityTracker.addMovement(ev);
    	switch (action & MotionEvent.ACTION_MASK) {
    	case MotionEvent.ACTION_DOWN: {
    		mActivePointerId = ev.getPointerId(0);
    		final int x = (int) ev.getX();
    		final int y = (int) ev.getY();
    		int motionPosition = pointToPosition(x, y);
    		if (!mDataChanged) {
    			if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
    					&& (getAdapter().isEnabled(motionPosition))) {
    				// User clicked on an actual view (and was not stopping a
    				// fling). It might be a
    				// click or a scroll. Assume it is a click until proven
    				// otherwise
    				mTouchMode = TOUCH_MODE_DOWN;
    				// FIXME Debounce
    				if (mPendingCheckForTap == null) {
    					mPendingCheckForTap = new CheckForTap();
    				}
    				postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    			} else {
    				if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
    					// If we couldn't find a view to click on, but the down
    					// event was touching
    					// the edge, we will bail out and try again. This allows
    					// the edge correcting
    					// code in ViewRoot to try to find a nearby view to
    					// select
    					return false;
    				}
    
    				if (mTouchMode == TOUCH_MODE_FLING) {
    					// Stopped a fling. It is a scroll.
    					createScrollingCache();
    					mTouchMode = TOUCH_MODE_SCROLL;
    					mMotionCorrection = 0;
    					motionPosition = findMotionRow(y);
    					reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
    				}
    			}
    		}
    		if (motionPosition >= 0) {
    			// Remember where the motion event started
    			v = getChildAt(motionPosition - mFirstPosition);
    			mMotionViewOriginalTop = v.getTop();
    		}
    		mMotionX = x;
    		mMotionY = y;
    		mMotionPosition = motionPosition;
    		mLastY = Integer.MIN_VALUE;
    		break;
    	}
    	case MotionEvent.ACTION_MOVE: {
    		final int pointerIndex = ev.findPointerIndex(mActivePointerId);
    		final int y = (int) ev.getY(pointerIndex);
    		deltaY = y - mMotionY;
    		switch (mTouchMode) {
    		case TOUCH_MODE_DOWN:
    		case TOUCH_MODE_TAP:
    		case TOUCH_MODE_DONE_WAITING:
    			// Check if we have moved far enough that it looks more like a
    			// scroll than a tap
    			startScrollIfNeeded(deltaY);
    			break;
    		case TOUCH_MODE_SCROLL:
    			if (PROFILE_SCROLLING) {
    				if (!mScrollProfilingStarted) {
    					Debug.startMethodTracing("AbsListViewScroll");
    					mScrollProfilingStarted = true;
    				}
    			}
    			if (y != mLastY) {
    				deltaY -= mMotionCorrection;
    				int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
    				// No need to do all this work if we're not going to move
    				// anyway
    				boolean atEdge = false;
    				if (incrementalDeltaY != 0) {
    					atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
    				}
    				// Check to see if we have bumped into the scroll limit
    				if (atEdge && getChildCount() > 0) {
    					// Treat this like we're starting a new scroll from the
    					// current
    					// position. This will let the user start scrolling back
    					// into
    					// content immediately rather than needing to scroll
    					// back to the
    					// point where they hit the limit first.
    					int motionPosition = findMotionRow(y);
    					if (motionPosition >= 0) {
    						final View motionView = getChildAt(motionPosition - mFirstPosition);
    						mMotionViewOriginalTop = motionView.getTop();
    					}
    					mMotionY = y;
    					mMotionPosition = motionPosition;
    					invalidate();
    				}
    				mLastY = y;
    			}
    			break;
    		}
    		break;
    	}
    	case MotionEvent.ACTION_UP: {
    		switch (mTouchMode) {
    		case TOUCH_MODE_DOWN:
    		case TOUCH_MODE_TAP:
    		case TOUCH_MODE_DONE_WAITING:
    			final int motionPosition = mMotionPosition;
    			final View child = getChildAt(motionPosition - mFirstPosition);
    			if (child != null && !child.hasFocusable()) {
    				if (mTouchMode != TOUCH_MODE_DOWN) {
    					child.setPressed(false);
    				}
    				if (mPerformClick == null) {
    					mPerformClick = new PerformClick();
    				}
    				final AbsListView.PerformClick performClick = mPerformClick;
    				performClick.mChild = child;
    				performClick.mClickMotionPosition = motionPosition;
    				performClick.rememberWindowAttachCount();
    				mResurrectToPosition = motionPosition;
    				if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
    					final Handler handler = getHandler();
    					if (handler != null) {
    						handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
    								: mPendingCheckForLongPress);
    					}
    					mLayoutMode = LAYOUT_NORMAL;
    					if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
    						mTouchMode = TOUCH_MODE_TAP;
    						setSelectedPositionInt(mMotionPosition);
    						layoutChildren();
    						child.setPressed(true);
    						positionSelector(child);
    						setPressed(true);
    						if (mSelector != null) {
    							Drawable d = mSelector.getCurrent();
    							if (d != null && d instanceof TransitionDrawable) {
    								((TransitionDrawable) d).resetTransition();
    							}
    						}
    						postDelayed(new Runnable() {
    							public void run() {
    								child.setPressed(false);
    								setPressed(false);
    								if (!mDataChanged) {
    									post(performClick);
    								}
    								mTouchMode = TOUCH_MODE_REST;
    							}
    						}, ViewConfiguration.getPressedStateDuration());
    					} else {
    						mTouchMode = TOUCH_MODE_REST;
    					}
    					return true;
    				} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
    					post(performClick);
    				}
    			}
    			mTouchMode = TOUCH_MODE_REST;
    			break;
    		case TOUCH_MODE_SCROLL:
    			final int childCount = getChildCount();
    			if (childCount > 0) {
    				if (mFirstPosition == 0
    						&& getChildAt(0).getTop() >= mListPadding.top
    						&& mFirstPosition + childCount < mItemCount
    						&& getChildAt(childCount - 1).getBottom() <= getHeight()
    								- mListPadding.bottom) {
    					mTouchMode = TOUCH_MODE_REST;
    					reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
    				} else {
    					final VelocityTracker velocityTracker = mVelocityTracker;
    					velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    					final int initialVelocity = (int) velocityTracker
    							.getYVelocity(mActivePointerId);
    					if (Math.abs(initialVelocity) > mMinimumVelocity) {
    						if (mFlingRunnable == null) {
    							mFlingRunnable = new FlingRunnable();
    						}
    						reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
    						mFlingRunnable.start(-initialVelocity);
    					} else {
    						mTouchMode = TOUCH_MODE_REST;
    						reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
    					}
    				}
    			} else {
    				mTouchMode = TOUCH_MODE_REST;
    				reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
    			}
    			break;
    		}
    		setPressed(false);
    		// Need to redraw since we probably aren't drawing the selector
    		// anymore
    		invalidate();
    		final Handler handler = getHandler();
    		if (handler != null) {
    			handler.removeCallbacks(mPendingCheckForLongPress);
    		}
    		if (mVelocityTracker != null) {
    			mVelocityTracker.recycle();
    			mVelocityTracker = null;
    		}
    		mActivePointerId = INVALID_POINTER;
    		if (PROFILE_SCROLLING) {
    			if (mScrollProfilingStarted) {
    				Debug.stopMethodTracing();
    				mScrollProfilingStarted = false;
    			}
    		}
    		break;
    	}
    	case MotionEvent.ACTION_CANCEL: {
    		mTouchMode = TOUCH_MODE_REST;
    		setPressed(false);
    		View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
    		if (motionView != null) {
    			motionView.setPressed(false);
    		}
    		clearScrollingCache();
    		final Handler handler = getHandler();
    		if (handler != null) {
    			handler.removeCallbacks(mPendingCheckForLongPress);
    		}
    		if (mVelocityTracker != null) {
    			mVelocityTracker.recycle();
    			mVelocityTracker = null;
    		}
    		mActivePointerId = INVALID_POINTER;
    		break;
    	}
    	case MotionEvent.ACTION_POINTER_UP: {
    		onSecondaryPointerUp(ev);
    		final int x = mMotionX;
    		final int y = mMotionY;
    		final int motionPosition = pointToPosition(x, y);
    		if (motionPosition >= 0) {
    			// Remember where the motion event started
    			v = getChildAt(motionPosition - mFirstPosition);
    			mMotionViewOriginalTop = v.getTop();
    			mMotionPosition = motionPosition;
    		}
    		mLastY = y;
    		break;
    	}
    	}
    	return true;
    }

    这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,那么我们就只看这部分代码就可以了。


    可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解了,喜欢寻根究底的朋友们可以自己去源码里找一找原因。


    这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并没有什么太多需要注意的东西,唯一一点非常重要的就是第92行调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。那么我们进入到这个方法中瞧一瞧,代码如下所示:

    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    	final int childCount = getChildCount();
    	if (childCount == 0) {
    		return true;
    	}
    	final int firstTop = getChildAt(0).getTop();
    	final int lastBottom = getChildAt(childCount - 1).getBottom();
    	final Rect listPadding = mListPadding;
    	final int spaceAbove = listPadding.top - firstTop;
    	final int end = getHeight() - listPadding.bottom;
    	final int spaceBelow = lastBottom - end;
    	final int height = getHeight() - getPaddingBottom() - getPaddingTop();
    	if (deltaY < 0) {
    		deltaY = Math.max(-(height - 1), deltaY);
    	} else {
    		deltaY = Math.min(height - 1, deltaY);
    	}
    	if (incrementalDeltaY < 0) {
    		incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
    	} else {
    		incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
    	}
    	final int firstPosition = mFirstPosition;
    	if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
    		// Don't need to move views down if the top of the first position
    		// is already visible
    		return true;
    	}
    	if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
    		// Don't need to move views up if the bottom of the last position
    		// is already visible
    		return true;
    	}
    	final boolean down = incrementalDeltaY < 0;
    	final boolean inTouchMode = isInTouchMode();
    	if (inTouchMode) {
    		hideSelector();
    	}
    	final int headerViewsCount = getHeaderViewsCount();
    	final int footerViewsStart = mItemCount - getFooterViewsCount();
    	int start = 0;
    	int count = 0;
    	if (down) {
    		final int top = listPadding.top - incrementalDeltaY;
    		for (int i = 0; i < childCount; i++) {
    			final View child = getChildAt(i);
    			if (child.getBottom() >= top) {
    				break;
    			} else {
    				count++;
    				int position = firstPosition + i;
    				if (position >= headerViewsCount && position < footerViewsStart) {
    					mRecycler.addScrapView(child);
    				}
    			}
    		}
    	} else {
    		final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
    		for (int i = childCount - 1; i >= 0; i--) {
    			final View child = getChildAt(i);
    			if (child.getTop() <= bottom) {
    				break;
    			} else {
    				start = i;
    				count++;
    				int position = firstPosition + i;
    				if (position >= headerViewsCount && position < footerViewsStart) {
    					mRecycler.addScrapView(child);
    				}
    			}
    		}
    	}
    	mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
    	mBlockLayoutRequests = true;
    	if (count > 0) {
    		detachViewsFromParent(start, count);
    	}
    	offsetChildrenTopAndBottom(incrementalDeltaY);
    	if (down) {
    		mFirstPosition += count;
    	}
    	invalidate();
    	final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    	if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
    		fillGap(down);
    	}
    	if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
    		final int childIndex = mSelectedPosition - mFirstPosition;
    		if (childIndex >= 0 && childIndex < getChildCount()) {
    			positionSelector(getChildAt(childIndex));
    		}
    	}
    	mBlockLayoutRequests = false;
    	invokeOnItemScrollListener();
    	awakenScrollBars();
    	return false;
    }

    这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实我们就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了。如第34行代码所示,如果incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动。


    下面将会进行一个边界值检测的过程,可以看到,从第43行开始,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,第47行当中,如果该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。


    接下来在第76行,会根据当前计数器的值来进行一个detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念当中,所有看不到的View就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。紧接着在第78行调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作为参数传入,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。


    然后在第84行会进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,如下所示:

    /**
     * Fills the gap left open by a touch-scroll. During a touch scroll,
     * children that remain on screen are shifted and the other ones are
     * discarded. The role of this method is to fill the gap thus created by
     * performing a partial layout in the empty space.
     * 
     * @param down
     *            true if the scroll is going down, false if it is going up
     */
    abstract void fillGap(boolean down);
    OK,AbsListView中的fillGap()是一个抽象方法,那么我们立刻就能够想到,它的具体实现肯定是在ListView中完成的了。回到ListView当中,fillGap()方法的代码如下所示:
    void fillGap(boolean down) {
        final int count = getChildCount();
        if (down) {
            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                    getListPaddingTop();
            fillDown(mFirstPosition + count, startOffset);
            correctTooHigh(getChildCount());
        } else {
            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                    getHeight() - getListPaddingBottom();
            fillUp(mFirstPosition - 1, startOffset);
            correctTooLow(getChildCount());
        }
    }
    down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:
    /**
     * Obtain the view and add it to our list of children. The view can be made
     * fresh, converted from an unused view, or used as is if it was in the
     * recycle bin.
     *
     * @param position Logical position in the list
     * @param y Top or bottom edge of the view to add
     * @param flow If flow is true, align top edge to y. If false, align bottom
     *        edge to y.
     * @param childrenLeft Left edge where children should be positioned
     * @param selected Is this position selected?
     * @return View that was added
     */
    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            // Try to use an exsiting view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }
        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }
    不管怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。


    既然getActiveView()方法返回的值是null,那么就还是会走到第28行的obtainView()方法当中,代码如下所示:

    /**
     * Get a view and have it show the data associated with the specified
     * position. This is called when we have already discovered that the view is
     * not available for reuse in the recycle bin. The only choices left are
     * converting an old view or making a new one.
     * 
     * @param position
     *            The position to display
     * @param isScrap
     *            Array of at least 1 boolean, the first entry will become true
     *            if the returned view was taken from the scrap heap, false if
     *            otherwise.
     * 
     * @return A view displaying the data associated with the specified position
     */
    View obtainView(int position, boolean[] isScrap) {
    	isScrap[0] = false;
    	View scrapView;
    	scrapView = mRecycler.getScrapView(position);
    	View child;
    	if (scrapView != null) {
    		child = mAdapter.getView(position, scrapView, this);
    		if (child != scrapView) {
    			mRecycler.addScrapView(scrapView);
    			if (mCacheColorHint != 0) {
    				child.setDrawingCacheBackgroundColor(mCacheColorHint);
    			}
    		} else {
    			isScrap[0] = true;
    			dispatchFinishTemporaryDetach(child);
    		}
    	} else {
    		child = mAdapter.getView(position, null, this);
    		if (mCacheColorHint != 0) {
    			child.setDrawingCacheBackgroundColor(mCacheColorHint);
    		}
    	}
    	return child;
    }

    这里在第19行会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。


    那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,然后我们在第22行将它作为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getView()方法示例:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
    	Fruit fruit = getItem(position);
    	View view;
    	if (convertView == null) {
    		view = LayoutInflater.from(getContext()).inflate(resourceId, null);
    	} else {
    		view = convertView;
    	}
    	ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
    	TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
    	fruitImage.setImageResource(fruit.getImageId());
    	fruitName.setText(fruit.getName());
    	return view;
    }
    第二个参数就是我们最熟悉的convertView呀,难怪平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?


    之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。


    为了方便大家理解,这里我再附上一张图解说明:




    那么到目前为止,我们就把ListView的整个工作流程代码基本分析结束了,文章比较长,希望大家可以理解清楚,下篇文章中会讲解我们平时使用ListView时遇到的问题,感兴趣的朋友请继续阅读 Android ListView异步加载图片乱序问题,原因分析及解决方案 。


    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • 第一部分 To B or not to ...B端产品即要符合商业组织的战略要求,能够满足商业用户需求,将已有商业运行逻辑进行系统化、信息化、高效化处理。两类都是为企业流程效率服务,让分散的、低效的个体,更好地连接合作,...

    很幸运的是2019年3月份读完了这本B端产品经理必修课,今天也就是2019年11月25日整理书籍再次拿出来看的时候,自己已经身在小米,主要是我当时忘记这本书的作者就是现在的同事宽同学了,了解其人,更要从书中再去品味。

    产品经理的沟通技巧:沟通、说服、谈判、演讲、辩论。

    ABC理论:假设影响行为,行为最终影响结果。在沟通过程中要以结果为导向,抛弃偏见,以开放的态度与每个人沟通。

    绊倒产品经理的6个绳索:

    1. 太在意过程。要明确目标,以结果为导向。
    2. 胡言乱语。要明确沟通目的。
    3. 不推不动。积极主动是一个好习惯。
    4. 不学习。多读书、看新闻、参加沙龙,不要停止好奇与学习。
    5. 焦头烂额:要学会时间管理,分清优先级。

    产品经理:为创造价值而生。案例:《未来的传奇——波音747的故事》

     

    第一部分 To B or not to B

    1 B 端产品经理  

    如何理解B端产品?

    B端产品主要分为两大类:

    • 为公司的管理服务,如:HR系统、OA系统;
    • 为公司的运营服务,如:供应链系统、ERP系统的。

    B端产品即要符合商业组织的战略要求,能够满足商业用户需求,将已有商业运行逻辑进行系统化、信息化、高效化处理。两类都是为企业流程效率服务,让分散的、低效的个体,更好地连接合作,发挥集成化的、系统化的更大作用。

    相较于C端产品,B端产品最大的特点是:面向特定领域用户,且数量少得多,但更注重对用户专业领域操作流程的深度挖掘——也就是专业性更强,与业务的结合更紧密。

    2  B 端产品经理的职业生涯

    B端产品经理工作:

    B端产品经理技能树:

    B端产品经理职业生涯:产品专员/产品助理>产品经理>高级产品经理>产品总监

    • 1.产品专员/产品助理:关注具体执行层面的协作,对产品的需求细化,以及对原型的设计和文档的整理
    • 2.产品经理:主要关注推动产品迭代、产品的实现与效果、数据和业务、感知业务和产品的发展方向。
    • 3.高级产品经理:主要关注商业价值和模式,以及和产品的全生命周期思考问题。
    • 4.产品总监:主要关注战略规划、业务发展以及团队管理。

    在这条职业发展路径的每个阶段关注的重点不同,要掌握的技能虽多,但不是每一种都需精通,可借鉴“二八原则”:真正重要的知识,或者在实践中被反复使用的知识,只占全部知识的20%。也就是说,20%的知识是需要反复修炼形成骨架的,剩下的80%在此基础上不断更新迭代。所以产品人要一直学习在路上。

    3  以精益思想为产品方法 

    花更少的人,更少的设备,更少的时间和空间为客户提供真正想要的东西。

    • 理念一:快。快时成本与效率的解决之道。
    • 理念二:流动产生价值。长时间没开发的需求就慢慢变得不实用要定期回顾他的价值或重新设计。
    • 理念三:采用最简单的方案。面对各有优劣方案举棋不定面对复杂流程而苦恼时,选取简单的方案是最优选择,结构简单的系统往往是最可靠的。
    • 理念四:处在联系中的事物才能被简化。简化不是减少,将需要简化的部分在系统中进行了转移。
    • 理念五:不害人的需求不是完整的需求。无论多坏的改变,都会有一些人收益;无论多好的改变,都会使一些人受损。在设计规则时,要多角度的考虑获益或者受损的角色。
    • 理念六:化散乱为规律,化应急为预测。懂得预测需求,否则就会疲于奔命。
    • 理念七:只可图示,不可言传。能用图表示的会更直观,更有助于发现问题。
    • 理念八:让公路排满车,就是堵车。将工作焦点转移到重要不紧急的事情上去。
    • 理念九:聚焦目标才能带来明确的结果。做产品如果想讨好所有的用户就会分散目标,变得平庸。
    • 理念十:持续改进,不忘初心。做出的产品方案需要不断优化,同时不断回顾最初目标防止跑偏。
    • 理念十一:细节体现专业。对事物的不断细分才能体现专业性。
    • 理念十二:不要造永动机。思考产品要从整体思考,不要陷入细节。
    • 理念十三:先准确,后精确。探索需求,先力求需求准确,再在此基础上精确的探索需求。

    第二部分 单个产品管理流程

    B端产品经理的工作流程归纳为五个阶段:

    • 产品规划→产品设计→产品研发→数据监控

    1. 规划阶段:基于组织的目标和战略,获取并分析需求,规划B端产品的发展方向和路径

    我们要从规划阶段开始设计我们的B端产品,在规划阶段,我们要开展市场调研、用户调研、产品路线规划、需求分析、需求管理等活动,这些活动分布在《用户体验要素》的战略层和范围层,即主要关注目标和实现目标的边界。

    这一阶段主要是产品经理要考虑的,作为刚入门的产品小白来说,可以先从了解行业动态开始,通过“人人都是产品经理”网站、喜马拉雅“36氪”等各种途径来了解,可以在茶余饭后与同事朋友聊聊,开拓思路。

    2. 设计阶段:基于需求和规划,设计产品信息架构、原型、交互、UI方案等

    在设计阶段,我们要开展设计信息架构,设计产品原型、设计交互、设计UI等活动。这些活动分布在《用户体验要素》的结构层、框架层和表现层,即要在界定的边界内勾划出最终输出物大体轮廓和具体执行方案及最终的输出物—产品。

    这一阶段涉及到具体执行层面,也是产品专员或产品助理应该重点关注的环节。目前阶段产品经理已经通过规划和分析需求了解到用户想做什么了,这一阶段即让概念进入产品化阶段。先不要急打开axure,我们需要先梳理出业务流程图和信息架构图,在此基础上再去进行细化,为了防止我们画原型时缺页面可以先梳理出页面流程图,最后再一气呵成完成你的原型设计。

    3. 研发阶段:根据已经设计好的产品方案,设计技术实现方案及推动产品研发。

    我们完成了设计阶段的工作后,将进入到研发阶段。在研发阶段,产品经理要协助研发开展产品开发工作。这个活动分布在《用户体验要素》的表现层,即关注最终的产出物—产品。虽说产品经理不需要写代码,但要承担项目管理、协助研发理清需求、协助测试开展测试以推进产品开发。

    在这个过程中,需要随时随地解答技术人员对需求的疑问以及协助测试人员将优化和bug分类整理,并安排优先级进行分批处理。

    4. 发布阶段:制订产品发布前的部署和培训计划,推动产品上线。

    B端产品在完成研发后,将进入发布阶段。在发布阶段,产品经理要开展制定产品发布方案、发布产品的活动。这些活动分布在《用户体验要素》的框架层和表现层。即关注具体的业务流程和最终的产品。

    在这个阶段前,要确认以下信息做好充分准备:

    • 1.产品是否具备待上线条件,比如是否有测试报告,是否得到使用方的验收通过;
    • 2.产品的操作手册和培训安排是否完成;
    • 3.产品上线时间是否合适,确保不要影响其他业务的操作。其他细节这里不赘述。

    5. 监控阶段:监控产品上线后的效果,收集并分析用户反馈的信息,并形成新的需求。

    在发布B端产品之后,产品经理将进入监控阶段。在监控阶段,产品经理要开展制定关键指标、收集及分析反馈信息的活动。这些活动分布在《产品体验要素》的框架层和表现层,即主要关注具体的业务流程和最终的产品。在监控阶段,产品经理要使用数据来监控产品上线后的效果,以及收集用户的反馈意见,最终为开启新的单个产品管理流程做准备。

    上线一段时间后,需要产品经理写上线邮件,主要目的有三个:

    • 1.总结与记录:总结项目过程,未来翻查资料速度超快;
    • 2.项目推动:产品上线后才是开始,需要推动、协调各方资源;
    • 3.团队润滑剂:给参与者帮助你的人正面反馈。监控阶段手机的新需求和反馈进行整理分析,用于后期优化产品。

    总体来说,B端产品经理主要关注3个方面:表现层、领域层、数据层。

    • 表现层:即用户界面,用户直接与系统进行交互和操作;
    • 领域层:是商业和业务逻辑,是核心关注点;
    • 数据层:关注是系统之间的交互与数据存储,系统之间会以接口的形式传送数据,关注接口传输性能、传输内容等。

    4 规划阶段:产品设计的开始 

    一、产品规划:调研市场→调研用户→规划产品路线→分析需求→管理需求

    在规划行动方案之前,一定要记得先问自己:有什么事情我“今天”做了,可以让“明天”更好,或者至少让“明天”不会更糟。

    1.调研市场:找B端竞品。

    目的:分析产品可能存在的盈利点,获取行业经验和方向

    需要:产品创意、行业信息

    方法:商业模式画布、SWOT分析、竞品分析

    指标:竞品分析报告、商业需求文档

    • 明确目的。想清楚你要查询的信息,定好方向再起步。
    • 与业务同事沟通。咨询业务同学竞品名字。
    • 了解专有名词。如ERP、WMS,通过搜索专有名词找到可用资料。
    • 找到同类SaaS产品。
    • 搜索信息渠道。知乎、简书,知网、万网。

    2.调研用户:倾听用户声音。

    目的:分析和研究产品使用者

    需要:竞品分析报告、商业需求文档、产品创意

    方法:用户研究方法(问卷调研、用户访谈等)

    指标:用户调研报告

    • 用户的话不能全信。为了引起重视而故意夸大,害羞或怕说错话不去表达真实想法。
    • 能有的功能,用户都希望有。人性贪婪。
    • 明确词语含义。“我希望报表更快一点”“更快一点”就需要进一步明确。
    • 尽量不要问有固定选项的问题。列选项即使没有他也会选择。不认让用户给选项打分0-10分。
    • 重述用户所说。将用户的话用产品经理的语言再说一遍,让用户判断说的对不对。
    • 别让用户预测。不要让用户设计产品,与未来相比用户当下的行为更有准确性。

    师徒制,三段式问法:请教>刨根问底>核实

    1)发现问题:你正在做什么事情?做的过程中有什么不舒服的吗?遇到了什么问题?

    2)分析流程:你现在用什么方法来解决整个问题?

    3)探索机会:为了更好的解决整个问题,你认为有什么方法可以帮到你?或者哪些地方可以优化下?

    3.规划产品路线:缩小现在与未来的差距  

    目的:规划产品路线、节奏

    需要:竞品分析报告、商业需求文档、产品创意、用户调研报告

    方法:

    • ①列出为了缩小差距所要做的事情
    • ②目前产品的约束条件,找出其中能做到的事情
    • ③预测这些事情会使产品有怎么样的结果
    • ④给这些结果排序,给他们加上一个期望日期

    指标:产品发展路线图Roadmap(实现时间、名称、目标、功能、优先级、度量标准)

    • 时间:完成时间是什么时候
    • 名称:实现的产品名称和版本号是什么
    • 目标:要实现什么样的目标,以及想要获得的收益
    • 功能:实现的功能是什么优先级这些功能的优先级是什么指标用什么标准来衡量已经完成并实现的计划

    有产品目标后要做好目标管理,规划行动方案,实时反馈并验收成果。

    1、分析和预测需求。

    产品经理首先要明确与产品成败相关的因素。要了解用户对各因素的期望。之后用现在和未来的时间维度去分析获得信息。从现在和未来的角度发现差异,目前用户从我们产品获得什么?是否让其满意?接下来用户还希望产品有哪些功能?

    2、现状分析。

    分析目前自己的产品处于什么状态。目前该产品与行业优秀产品有什么区别?

    3、缩小差距。

    • a、用头脑风暴列出为缩小差距所要做的事情。
    • b、思考从目前的约束条件列出清单中可以做的事情。
    • c、已经选出的事情会使产品有怎样的结果,最好能够测量。
    • d、给结果排序,列出优先级及期望实现日期。

    4.分析需求:用图形代言需求   

    目的:将需求具体化

    需要:竞品分析报告、商业需求文档、产品创意、用户调研报告

    方法:筛选需求(需求蛋模型)→思考需求(D×V×F>R)→解析需求(UML统一建模语言)

    指标:需求说明文档

    需求应有的特征:

    • 痛点:好的需求犹如根治用户痛处的良药。B端产品通过调研用户基本可提炼出痛点。
    • 收益:需求应有可量化的结果导向。
    • 明确、可行、简单的第一步:挖掘需求就是降低需求中的含混性,使之明确。如果在需求落地成型阶段才发现含混性,这个时候的改正成本实在是太高了。

    需求的变革公式:不满情绪*变革愿景*初步实践>变革阻力。对现状的不满、对变革的期盼、愿意迈出明确的第一步等其中任何一个因素没做到将导致变革失败。

    • 需求的可行性=(需求的当前价值+未来价值)/(需求的实现成本+维护成本)
    • 解析需求:数据驱动,行为产生数据,数据联系行为。数据流动形成数据流,从而把业务中的人联系在一起。

    举例设计一个咖啡馆的管理系统

    • 1、画流程图先把主要流程总结出来。

    进店——点餐——下单——制作食物——送餐——就餐——结账——离店

    • 2、对主要流程进行细化。如果流程图中的活动数量超过7+-2的范围,则颗粒度太细或太粗。

    比如将点餐流程进行细化

    • 3、实体关系图(ER图)

    数据之间三种对应关系:一对一、一对多、多对多

    • 一对一:顾客就餐完成后需要支付自己的账单。顾客1——1账单
    • 一对多:服务员工作可以为多个顾客服务。服务员1——n顾客
    • 多对多:面包、咖啡等可以被不同客人点单。菜品n——n顾客

    数据对象的属性也是一类数据,用来描述数据对象,并且多个数据对象可以包含相同的属性。如何区分数据对象和属性:xx单、xx表一般都是数据对象,数据对象区别于其他实物独立存在的个体,数据对象一般能用量词“类”来形容。数据对象的属性数据可被用来增删查改,可以通过该来查漏补缺。

    • 4、数据流程图

    数据:表示数据流,连接数据流程图的各元素。

    外部实体:外部实体表示系统之外的人或事物,它可以成为整个数据流的起点或者终点。

    数据储存:存储数据的区域。在现实中,可能是单或者表格表格。

    活动操作:对数据进行操作,包括数据的流入和流出。

    • 5、用例图

    用例是对产品功能需求的描述。

    需求文档

    • 1、需求名称
    • 2、背景
    • 3、目标与收益
    • 4、功能需求。业务概念、流程展示、需求描述。
    • 5、非功能需求

    5.管理需求:打造简单可实践的需求池 

    目的:将需求具体化

    需要:产品发展路线图、需求说明文档、产品创意

    方法:需求收集(急诊模式)→需求设计(登机模式)→需求研发(看板模式)

    指标:需求池、需求排期计划

     

    需求的重要性:为了区分同一优先级的多个需求,可以用重要性来辅助优先级管理需求。

    重要性就是对需求进行打分,分数范围是1—100分(根据5个优先级可以分成5等分),每个需求的分数是唯一的。优先做分数大的需求。

    优先级和重要性一旦确定,所有的资源将向这些需求倾斜。处理跨部门的需求时,使用优先级尤为重要,但重要性的分数不能跨部门比较。

    5 设计阶段:产品从概念到解决方案

    产品设计:设计产品架构→设计产品原型→设计交互→设计 UI

    1.设计产品架构:设计让产品立得住的骨架 

    需要:产品发展路线图、需求说明文档、需求排期计划

    方法:   设计信息架构(三要素:情景、内容、用户)→输出站点地图(UML)

    指标: 站点地图

    信息架构(收纳信息)

    • 信息架构三要素:情景,内容、用户。
    • 信息架构五组件:组织系统、标签系统、导航系统、搜索系统,
    • 组织信息:根据时间字母等对信息进行组织分类。
    • 给信息加标签:用一个名称对大量的信息进行概括,就是给信息加入了标签,便于快速查询。
    • 设置找到信息的路径:导航
    • 搜索信息:搜索功能
    • 描述信息的特征:通过各种条件筛选数据。

    站点地图(原型设计起点)

    • 各页面的层级关系。b端产品的四种基本页面类型:表单页、详情页、列表页、Dashboard页
    • 表单页:用户向系统增加、删除、提交信息的操作页面。
    • 详情页:展示详细信息。
    • 列表页:向用户展示结构化的数据信息。列表页的设计大部分来自用户对实际数据的操作和展示。
    • Dashboard页:仪表盘,监控系统运营情况。

    2.设计产品原型:高效产出原型的方法

    需要:站点地图、需求说明文档

    方法:   交互设计、排版、axure 技能、

    指标: 产品原型、PRD 文档

    模式思维

    模式,指可以重复使用的方式和方法。类似于乐高积木原理。

    以厨房设计为例,厨房空间需要炉灶、水槽、食物储存区、操作台四个区域。以上四个部分距离不能太大在3m以内,操作台的范围大致在1.2-3.6m。要在一个页面上满足用户多种活动需求,比如信息查看、搜索、下载等,每种活动对应一种解决方案,这个解决方案就是模式。设计模式是由组件组成,组件是构成设计模式的基本元素。

    因此设计产品原型的流程:

    • 1、根据站点地图,找到要设计的页面类型。(列表页、表单页、详情页、Dashbard页等)
    • 2、根据页面类型对应用户操作行为,思考出各自对应的模式。
    • 3、用组件搭建成对应的模式。各种模式的布局和组合最终形成产品原型。

    总结属于自己的设计模式

    • 1、模式名称:给自己的模式起个名称,便于管理交流。比如,搜索单据。
    • 2、概念和价值:描述清楚这个模式是什么,即给模式下一个定义。写清楚给用户带来什么价值。
    • 3、使用范围:该模式相关的边界条件。比如在用户登录情况下,向用户推荐常用信息。
    • 4、模式描述:用文字图片等形式描述清楚模式由哪些组件构成及该模式是如何运行的。如:用户在输入框录入关键词时,会实时展示提示信息,便于用户选择。
    • 5、相关模式:与这个模式相关的模式还有哪些。

    三种精度的产品原型展示

    • 低精度原型:即页面流程图,展示页面中的关键组件及页面之间的跳转流程。
    • 中精度产品原型:像照片一样,展示包含所有组件的页面,主要展现页面布局。
    • 高精度产品原型:详细展示原型中各个组件在不同操作下所展示的信息。

    需求文档加上 网站地图及产品原型就为产品需求文档。

    3.设计交互:让B 端产品简单易用  

    B端产品更加偏重于工具属性,注重帮助用户完成工作效率和效果。所以,设计C端产品的交互更像是设计一本赏心悦目的小说,设计B端产品更像是一本产品说明书,需要追求使用的高效和易学性。

    4. 设计UI:如何与设计师高效沟通 

    跟设计师的合作注意以下几点:

    1. 主动学习设计知识,如:常逛逛Dribble、优设、站酷之类的设计网站,提高自己对设计的认知。同时,了解公司或团队的设计规范。
    2. 明确指出设计重点,表达顺序。明确页面中重点功能是什么,使用者在什么场景下使用,以及希望用户重点使用的界面组件和信息有哪些。
    3. 给出设计案例。可以找一些比较好的设计案例给设计师参考,指出案例中哪些元素可以参考。

    尼尔森十大可用性原则

    • 系统状态可见:用户能够随时获得产品反馈的信息,会让用户产生对产品的信任和安全感。
    • 系统与真实世界匹配:要参考真实环境使用的单据和报表,将其映射在产品中。
    • 用户掌控和自由操作:用户可以自由退防护或者结束当前任务。
    • 一致性和标准化:让界面元素和操作形成一套让用户可识别、可学习的标准,并且在产品的任何地方都可以应用。
    • 避免错误:需要检查一下界面的按钮是否可能产生误触。
    • 直接识别比记忆好:产品要减少用户的记忆负担。
    • 灵活高效地使用:要不断地提高界面使用效率
    • 美观和简约的设计:设计要简明突出。
    • 帮助用户识别、诊断和解决错误:着重关注给用户反馈的操作信息,且尽可能以友善的态度表达。
    • 帮助和文档:需要在界面上提供必要的使用帮助,并整理出专门的产品使用文档帮助用户学习。

    6 研发阶段:产品方案的实现

    产品研发:项目启动→规划→执行→监控→收尾

    1.项目启动

    说明项目目标、阶段划分、组织结构、管理流程等关键事项

    2.规划

    明确研发工作内容以及各需求点的研发、测试负责人,评估研发时间,制定排期计划表

    c74f78d09f414bb4c6fdc280e845e405-picture

    3.执行  (一个Java项目的标准开发流程

    总体设计→概要设计→详细设计→编写代码→代码审核→单元测试→集成测试→系统测试→发版上线

    4.监控

    对项目输出成果或者阶段性成果进行检查,看看是不是我们想要的或是缺少了什么

    PS:需求看板可以有效管理各需求进度,防止需求堆积拥堵导致项目不能按时交付

    deabb9c4f7e461fec6e0088f7e51a8b6-picture

    5.收尾

    试用、培训、维护、项目回顾复盘

    项目管理

    在研发阶段,产品经理需要承担起项目管理的义务,协助研发和测试同事,以推进产品开发。

    项目管理的四个维度:范围、时间、质量、成本。

    可对应的项目目标:多、快、好、省。

    1、核心问题,什么是项目?项目是为创造独特的产品、服务或者成果而进行的临时性工作。据此对项目有三个定义。

    • 项目有明确的开始和结束,也就是项目有明确的开始时间和结束时间。没有明确开始时间和结束时间的活动称之为运营。运营是一个通过连续不断的工作来交付成果。
    • 项目会产生成果。最终提供用户使用的产品功能。
    • 项目计划随着项目的开展而逐渐详细。项目会随着计划的开展展现很多之前未考虑到的细节

    2、项目目标,多、快、好、省在将要延期的情况下可以考虑砍掉部分功能而不要增加开发资源。

    3、项目计划:

    项目风险管理:

    项目风险:如果发生不确认的条件和时间,会对一个或多个项目目标造成影响。

    项目沟通:

    原则:不论采用何种手段,邮件、微信、电话、面谈,信息的发出方一定要保证接收方能够收到并且理解信息,做出反馈。

    项目推进:

    推进项目的重要基石是:标准化——标准化指完成某项工作的最佳工作方法。

    产品经理可以将项目过程遇到的问题及处理方法、人员配合方式、项目流程等经验或文档分享给其他项目成员,推而广之,达成大家的共识。

    比如:

    • 项目会议纪要模板:帮助大家高效输出内容完备的会议纪要。
    • 上线验收清单模板:让大家按照清单和步骤执行可以减少出错、提高效率。
    • 项目工作流:明确各自角色的任务及配合时间点,团队配合更紧密。

    标准化可以避免项目再次陷入相同的错误中。沿用成功的工作方法、经验,让项目不断被顺利推进。

    而在研发日常跟进中,可以采用看板模式来记录和跟进。看板管理需要注意的就是:避免某个阶段的需求出现拥堵,或者是一旦发现拥堵,要及时疏解。

     需求卡片可以包含如下信息(工具Trello、Teambition)

    • 1、需求名称
    • 2、需求的相关人:需求人、负责人、产品经理、研发工程师
    • 3、需求类型:如需求涉及哪些系统、哪些部门等
    • 4、需求完成时间
    • 5、需求描述:可以附上产品文档
    • 6、需求优先级

    7 发布阶段:产品上线的临门一脚

    上线前需确认的信息:

    1. 产品是否具备上线条件,比如:是否有测试报告,是否得到使用方的验收。
    2. 产品的操作培训是否完成,或者是否至少有使用说明文档。
    3. 产品上线时间是否合适。产品上线的时间点是否会影响其他业务操作,是否需要配合整体的运营计划。

    产品发布:

    产品推广产品,可运用营销推广模型的核心思路:描述一个重要的问题,并让大家认同,之后介绍产品给出的解决方案。

    营销推广模型分7步:

    1. 背景介绍:介绍所发布产品的背景信息,比如:时间、地点、任务、事件等信息,便于大家了解背景知识,从而减少认知负担。
    2. 描述阻碍:描述用户目前会遇到的问题,并让大家认同该问题确实会给自己带来不便。
    3. 点燃希望:向大家说明这个问题有解决方案,引起打击的期待和注意。产品经理客户可以介绍这个问题的解决方案,及概念或者同行业对这个问题的解决思路。
    4. 震撼登场:抛出问题的解决方案——即发布的产品是什么。
    5. 展现价值:描述这样的解决方案和产品会给用户带来怎样的价值和收益,可以配数字,这样会更有说服力。
    6. 精雕细琢:介绍产品重要的细节、工作原理。
    7. 给出诱惑:给大家送一些福利,让大家快来体验产品。这里可以根据实际情况来选择使用。

    发布一款产品或者介绍一个功能并不都需要发布会的形式,产品经理可以应用简单有效的演讲框架快速打动用户。

    8 监控阶段:让产品不断生长  

    8.1 制订数据指标及目标:产品演进的航标  

    • 8.1.1 数据指标的黑箱和二律背反  
    • 8.1.2 关键成功因素法:制订数据目标的方法  

    8.2 收集及分析反馈信息:整装待发  

    • 8.2.1 零基础快速入门SQL 的方法  
    • 8.2.2 与用户座谈的产品回顾会  

    数据监控:制定关键指标→收集分析反馈信息

    1.制定关键指标

    方法:关键成功因素法,输出OGSM表(如图)

    定位长期目标→制定对应的短期目标→找到实现短期目标的关键成功因素(CSFs)→确定 CSFs 实施的测量方法

    5fcd58bd96f7eb1cdf86b13847f05510-picture

    2.收集及分析反馈信息

    产品回顾会:制定会议章程(会议邀请邮件)→展现事实(影响/问题)→集思广益(问题&方案)→决定做什么(总结行动项、负责人、deadline)→总结和公告(整理会议纪要)

    产品监控数据指标:

    数据监控应该监控什么才有意义,从黑箱、仪表盘和二律背反理论中我们可以得到一些启发。

    • 黑箱:

    我们只能输入和输出,而并不知道事物真正运行的原理是什么,比如:电商网站的输入是用户进站浏览,输出是订单。那用户在浏览网页所做的行为和决策就是黑箱。 通过研究黑箱,我们可以提升用户转化率。

    • 仪表盘:

    通过汽车的仪表盘速度、耗油量等数据指标,随时反馈出汽车的状态。没有仪表盘的汽车随时都有失控的危险。对系统运行状态的监控也是,数据指标要尽可能覆盖全面,比如:出现问题的次数、加载时间、业务相关数据等。

    • 二律背反:

    二律背反指规律中的矛盾,在互相联系的两种力量的运动规律之间存在的相互排斥现象——即两种事物此消彼长、此长彼消、相背相反。

    因此,我们除了关注数据指标之间的相关性,更需要找到这些处在二律背反的指标,然后进行指标配对。通过指标配对,防止过度监控或者提升一个指标而带来副作用,用另一个指标来辅助分析和监控,从而权衡出好的办法以解决问题。

    德鲁克说:如果没办法计量就没办法管理。数据指标就是管理量化的表现。监控部分数据指标来监控系统的运行状态,如B端产品数据指标包括出现问题的次数、加载时间、以及业务相关数据指标。

    制定数据目标思路:关键成功因素法

    • 定位长期目标。产品经理找到组织或者团队的长期目标是节省成本。
    • 为了实现长期目标,需要制定对应的短期目标。如在长期目标的基础上拆解出短期要完成的目标是减少包装成本。
    • 找到实现短期目标的关键成功因素。如实现减少包装成本的短期目标可以做的工作是系统推荐使用的包装盒形状等。
    • 确定关键成功因素实施的测量方法。找到所要实现目标所要做的事情后,需要一个标准来测量是否施行到位。如,我们使用推荐准确率达到90%的指标来监测。

    数据采集:

    SQL是查数据和做报表的工具,建议产品经理都要学:

    • SQL可能是最容易入门的编程语言。因为他书写出来的代码,完全是按照英语语法,是初中语法中最简单的部分。只要学习非常少的SQL知识,或者说是几个英语单词,就可以快速在工作中使用。
    • 使用频率非常高。
    • 有助于产品经理理解数据分析的思路。

    SQL 入门手段:

    • 《SQL基础教程》,这本书内容实用且基础,适合零基础的人学习,且它描绘了很多使用场景。
    • 学习编程的网站,如http://www.w3school.cn/ ,这里的教学内容简约便捷,可以当成SQL使用的工具字典
    • 找一名程序员同事当老师,随时实践、随时请教问题。

    在监控指标时需要注意的细节:

    制定数据目标的原则:具体、可衡量、可实现、有相关性、有截止时间。

    根据关键成功因素的分析思路最终输出OGSM表。OGSM是Obiective(长期目标)、Goal(短期目标)、Strategy(策略)、Measurement(测量方法)名词的首字母。形成另一种展现形式长期目标:节省成本短期目标策略/关键成功因素测量方案行动方案减少包装成本系统推荐包装盒形状系统推荐率达到90%的指标Q1完成功能研发产品回顾会

    • 制定会议章程。会议前产品经理发送邮件包括开会目标、会议议题、时间地点、会议流程、参与人员、准备资料等。产品经理要保障会议内容简单明了,以及重要人员可以出席。会议开始后产品经理也要重申会议章程内容,让与会者明确会议议题。
    • 展现事实。阐述与产品相关的实际情况,上线后对业务运营数据的影响,或产品出现的问题和故障有哪些?记录会议内容,控制好节奏。
    • 集思广益。陈述完事实后,讨论找到解决方案。
    • 决定做什么。讨论完成后产品总结会议后的行动项,以及行动的负责人和完成时间,便于会议内容的追踪和落实。
    • 总结和公告。会议收尾时,产品经理作为主持人要总结本次会议所有参会人都同意的重要结论。散会后将结论总结为会议纪要发给相关人来备忘。

    第三部分 产品经理的自我管理

    1、帕金斯定律人在做一件事情时,耗费的时间越长就会感到越累。

    2、产出=活动*杠杆率越符合组织、团队战略或目标的活动越具有高杠杆率。

    3、提升工作速度

    • 建立收件箱:产品经理不要被当前来的事情打断,让当前来的事情进入收件箱,然后分配时间去处理。
    • 把场景相同的活动放在一起做:把类似的事情集中在一起做,减少任务切换的时间。
    • 找到关键路径:产品经理在必须做的事情中可以插入可以并行的工作。
    • 制定每天的活动计划:每天开始要预想一天的工作,哪些是重点工作,哪些是可以放到一起做的工作。但也不要把每天的工作排的非常满,导致没有时间应对紧急情况。
    • 合理拒绝别人的猴子:对于做不了或者暂时没时间做的工作坚决说不。如果在接受事情前,没有评估好是否能保质完成,那就违背产品经理的职责。
    • 学会番茄工作法:没工作一段时间就休息一会。

    不主动,工作没有重点,求大求全是产品经理的绊脚石。

    4、产品力

    产品经理的技能可以分为硬技能和软技能。

    • 硬技能包括:用户调研、产品规划、需求分析与管理、产品方案设计、数据分析等。
    • 软技能包括:项目管理能力、时间管理能力、沟通能力等。

    5. 产品力的获得途径

    作者的个人情况或习惯:

    • 喜欢看书、涉猎历史、哲学、科学、经管、互联网、技术等各个领域的书籍。
    • 把写书列为一个长期的目标,规划了5年左右的时间。
    • 因为B端产品经理的知识没有成型理论和体系,所以,作者立志填补这项空白。期望自己的总结和思考,为中国产品经理职业发展提供理论和实践的支持。
    • 为了写书,查阅大量现有的互联网、经管类书籍,还有大量的软件工程类书籍,以及学术论文。
    • 查阅资料注重追溯知识本源,了解知识的核心要义。
    • 本书展示了作者对B端产品经理的理解,介绍了B端产品经理的工作流程、工作方法、工作场景,以及作者在工作中的经验总结。
    • 喜欢跟研发同事散步聊产品和设计,在无拘无束的畅谈中总结自己对产品的看法和观点。

    以上,可以了解到作者身上几个难能可贵的品质:

    • 对世界充满好奇。在好奇心的驱动下,习得的知识非常宽广。
    • 对产品工作发自肺腑的热爱。兴趣让学习、规划和实践更加纵深。
    • 目标驱动、规划落地。目标明确,为达目标时刻准备。
    • 喜欢追根溯源。了解知识时追求本源,学习知识时抓核心要义。
    • 持续学习,知识内化,不断总结和输出。工作实践不断总结成经验和方法,形成自己的理论体系,揉碎成通俗易懂的生活例子阐释。
    • 寻求好的实践经历。在好的项目、工作环境中迸发出更多的灵感和提升。

     

    展开全文
  • 可以看到,with()方法的重载种类非常多,既可以传入Activity,也可以传入Fragment或者是Context。每一个with()方法重载的代码都非常简单,都是先调用RequestManagerRetriever的静态get()方法得到一个...
  • 20.如何app业务逻辑提炼api接口

    万次阅读 2015-03-23 12:54:30
    在app后端的工作中,设计api是一个很考验设计能力的工作。在项目的初始阶段,只知道具体的业务逻辑,那怎么把业务逻辑抽象和提炼,设计出api呢?通过阅读本文,可解答以上疑惑。
  • 虽然很多时候一个api接口的业务,数据逻辑是后端提供的,但真正使用这个接口的是客户端,一个前端功能的实现流程与逻辑,有时候只有客户端的RD才清楚,某种意义来说,客户端算是接口的需求方。所以建议在前期接口...
  • ———— / BEGIN / ————一、当我们在讨论用户行为时,我们在说什么1.1 基础出发,回归初始定义很多日常脱口而出的词,其实我们并没有思考过它真实的含义。大多数争论和错误决策的起点,也在于定义的不清晰和...
  • 如何app业务逻辑提炼api接口

    千次阅读 2015-08-08 10:30:40
    在项目的初始阶段,只知道具体的业务逻辑,那怎么把业务逻辑抽象和提炼,设计出api呢?通过阅读本文,可解答以上疑惑。    在本文中,是用以前做过的app移客ekeo第一版(以后的业务逻辑改了很多)业务逻辑来...
  • 开发业务逻辑

    千次阅读 2015-07-21 10:52:46
      ...记得几个月前,在一次北京博客园俱乐部的活动上,最后一个环节是话题自由讨论。就是提几个话题,然后大家各自加入感兴趣的...当时我和大家讨论ASP.NET MVC的相关话题去了,就没能加入“业务逻辑”组的讨论
  • 一个典型的电商新零售的系统,到底长什么样子,又包括那些模块和功能组成,相信这是很多小于三岁的产品人的一个疑问,我们来看看全貌: ▲典型电商新零售的产品架构 1、基础数据 主要用于咱们日常所有的...
  • 论今日头条背后的产品逻辑分析

    千次阅读 2017-04-30 22:06:48
    论今日头条背后的产品逻辑分析
  • Qt的角度看MVC框架

    千次阅读 2017-01-27 15:29:31
    Qt的角度看MVC框架最近开始看《设计模式之禅》,其中MVC框架是我最熟悉的,文中的MVC是以JAVA的代码来解释的,我对java只是略有接触也就不赘述书本里的描述和代码分析,这里就Qt的角度来分析一下MVC框架和...
  • 在计算机产业发展的70年时间里,每一次的 IT 革命,无不带来:更低廉的价格、更完善的功能、更便捷的使用、更广阔的市场! 大数据经过10年发展,现在已经到了一个重要的分水岭阶段:通用性和兼容性能力成为大数据...
  • 下面我就技术的角度说说一下微信小游戏如何开发的。 附:本文适合有开发经验的人,关于如何创建微信小游戏账号和使用微信小游戏开发工具就不再赘述,不了解的可以到微信小游戏开发者后台阅读相关文档。 相关...
  • 研究证实,人类一出生即开始累积庞大且复杂的数据库,包括各种文字、数字、符码、味道、食物、线条、颜色、公式、声音等,大脑惊人的储存能力使我们累积了海量的资料,这些资料构成了人类的认知知识基础。...
  • 运维的角度看微服务和容器

    万次阅读 2016-08-31 08:55:18
    责编:钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,另有「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008申请入群,...
  • 现在越来越多的APP都加入了换肤功能或者是日间模式和夜间模式等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们都有日间模式和夜间模式的主题切换。...
  • oracle数据库的物理结构及逻辑结构

    千次阅读 2017-09-02 18:24:17
     Oracle数据库的物理结构由构成数据库的操作系统文件组成,它是操作系统的角度来分析数据库的组成,在操作系统中可以看得到的文件,也就是说它是数据库在操作系统中的存储位置。常见的物理结构包括:控制文件、...
  • 3.1 系统逻辑架构

    千次阅读 2018-10-15 11:11:40
    第3章 超级账本的系统架构区块链的业务需求多种多样,一些要求在快速达成网络共识及快速...下在企业级区块链系统中常见的模块构成一些常用的功能模块有:应用程序、成员管理、智能合约、账本、共识机制、事件机制...
  • 逻辑架构和物理架构

    千次阅读 2018-10-16 16:04:34
    逻辑架构和物理架构 在实际工作中,我们经常听到“架 构”和“架构师”这样的名词,并不新鲜,但是总让很多刚入门的人感觉很神秘,甚至是高深莫测。很少有人对“架构”有全面的了解和认识能并说清楚架构是什 么,更...
  •  在逻辑地址、线性地址和物理地址一节中,已经对逻辑地址、线性地址和物理地址的概念做了详细的讲解。现在在这篇文章中,我们可以详细的对段式、页式、段页式内存管理方式以及三种地址之间的转化做一个详细且深入的...
  • 一步一步开始FPGA逻辑设计 - 高速接口之PCIe

    万次阅读 多人点赞 2016-11-22 17:04:30
    近两年来和几个单位接触下来,发现PCIe还是一个比较常用的,有些难度的案例,主要是涉及面比较广,需要了解逻辑设计、高速总线、Linux和Windows的驱动设计等相关知识。 这篇文章主要针对Xilinx家V6和K7两个系列的...
  • 面试官角度告诉大家如何准备项目方面的描述

    万次阅读 多人点赞 2018-01-10 20:08:59
    在项目里,我们用了HashMap对象来存放键值对类型的对象,其中“键“是用户对象,值是这位用户的订单列表,所以我们就需要在用户对象的class里,重写hashcode和equals方法,否则会出错。 随后可以举个不重写hashcode...
  • 华为2020届逻辑笔试

    千次阅读 2020-09-06 14:35:18
    6、下面哪种不是组合逻辑电路功能描述方法( )。 A 真值表 B 布尔方程 C 状态机 D 逻辑框图 解析: A选项真值表是使用于逻辑中的一类数学用表,用来计算逻辑表示式在每种论证(即每种逻辑变量取值的组合)上的值。其...
  • 在Coding前,需要我们考虑好业务功能的分配,关注于功能会频繁变更的部分,为未来的维护和扩展打下良好基础。诚然,这确实是一个基础,限于当时的资源、环境等约束,难以将所有问题一步到位的解决,还待于后期的功能...
  • 如何建立和评估数据仓库逻辑模型

    千次阅读 2006-07-18 16:46:00
    最终应用的功能和性能的角度来看,数据仓库的数据逻辑模型也许是整个项目最重要的方面,需要领域专家的参与。内容上看,涉及的方面有确立主题域,粒度层次的划分,确定数据分割策略,关系模式的确定。 逻辑模型...
  • -- NO.11--这是Becomewiser的第11篇文章全文约7373字,建议先收藏再看数据分析的下限,取决于逻辑归纳。与其说提高分析质量,不如说提升逻辑归纳能力。逻辑归纳,需要拥...
  • 这个我还真不知道,这里涉及到操作硬件(手机屏幕)方面的知识,也就是Linux内核方面的知识,我也没有了解过这方面的东西,所以我们可能就往上层来分析分析,我们知道Android中负责与用户交互,与用户操作紧密相关的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 149,646
精华内容 59,858
关键字:

从用户角度或者逻辑功能