一行 Python代码实现并行

一行 Python代码实现并行
Python 在程序并行化方面多少有些声名狼藉。撇开技术上的问题,例如线程的实现和 GIL,我觉得错误的教学指导才是主要问题。
常见的经典 Python 多线程、多进程教程多显得偏”重”。而且往往隔靴搔痒,没有深入探讨日常工作中*有用的内容。
传统的例子
简单搜索下”Python 多线程教程”,不难发现几乎所有的教程都给出涉及类和队列的例子:
import os
import PIL
from multiprocessing import Pool
from PIL import Image
SIZE = (75,75)
SAVE_DIRECTORY = ‘thumbs’
def get_image_paths(folder):
    return (os.path.join(folder, f)
            for f in os.listdir(folder)
            if ‘jpeg’ in f)
def create_thumbnail(filename):
    im = Image.open(filename)
    im.thumbnail(SIZE, Image.ANTIALIAS)
    base, fname = os.path.split(filename)
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
    im.save(save_path)
if __name__ == ‘__main__’:
    folder = os.path.abspath(
        ’11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840′)
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
    images = get_image_paths(folder)
    pool = Pool()
    pool.map(creat_thumbnail, images)
    pool.close()
    pool.join()
哈,看起来有些像 Java 不是吗?
我并不是说使用生产者/消费者模型处理多线程/多进程任务是错误的(事实上,这一模型自有其用武之地)。只是,处理日常脚本任务时我们可以使用更有效率的模型。
问题在于…
首先,你需要一个样板类;
其次,你需要一个队列来传递对象;
而且,你还需要在通道两端都构建相应的方法来协助其工作(如果需想要进行双向通信或是保存结果还需要再引入一个队列)。
worker 越多,问题越多
按照这一思路,你现在需要一个 worker 线程的线程池。下面是一篇 IBM 经典教程中的例子——在进行网页检索时通过多线程进行加速。
#Example2.py
”’
A more realistic thread pool example
”’
import time
import threading
import Queue
import urllib2
class Consumer(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self._queue = queue
    def run(self):
        while True:
            content = self._queue.get()
            if isinstance(content, str) and content == ‘quit’:
                break
            response = urllib2.urlopen(content)
        print ‘Bye byes!’
def Producer():
    urls = [
        ‘http://www.python.org’, ‘http://www.yahoo.com’
        ‘http://www.scala.org’, ‘http://www.google.com’
        # etc..
    ]
    queue = Queue.Queue()
    worker_threads = build_worker_pool(queue, 4)
    start_time = time.time()
    # Add the urls to process
    for url in urls:
        queue.put(url)
    # Add the poison pillv
    for worker in worker_threads:
        queue.put(‘quit’)
    for worker in worker_threads:
        worker.join()
    print ‘Done! Time taken: {}’.format(time.time() – start_time)
def build_worker_pool(queue, size):
    workers = []
    for _ in range(size):
        worker = Consumer(queue)
        worker.start()
        workers.append(worker)
    return workers
if __name__ == ‘__main__’:
    Producer()
这段代码能正确的运行,但仔细看看我们需要做些什么:构造不同的方法、追踪一系列的线程,还有为了解决恼人的死锁问题,我们需要进行一系列的 join 操作。这还只是开始……
至此我们回顾了经典的多线程教程,多少有些空洞不是吗?样板化而且易出错,这样事倍功半的风格显然不那么适合日常使用,好在我们还有更好的方法。
何不试试 map
map 这一小巧精致的函数是简捷实现 Python 程序并行化的关键。map 源于 Lisp 这类函数式编程语言。它可以通过一个序列实现两个函数之间的映射。
urls = [‘http://www.yahoo.com’, ‘http://www.reddit.com’]
results = map(urllib2.urlopen, urls)
上面的这两行代码将 urls 这一序列中的每个元素作为参数传递到 urlopen 方法中,并将所有结果保存到 results 这一列表中。其结果大致相当于:
results = []
for url in urls:
    results.append(urllib2.urlopen(url))
map 函数一手包办了序列操作、参数传递和结果保存等一系列的操作。
为什么这很重要呢?这是因为借助正确的库,map 可以轻松实现并行化操作。
在 Python 中有个两个库包含了 map 函数:multiprocessing 和它鲜为人知的子库 multiprocessing.dummy.
这里多扯两句:multiprocessing.dummy?mltiprocessing 库的线程版克隆?这是虾米?即便在 multiprocessing 库的官方文档里关于这一子库也只有一句相关描述。而这句描述译成人话基本就是说:”嘛,有这么个东西,你知道就成.”相信我,这个库被严重低估了!
dummy 是 multiprocessing 模块的完整克隆,唯一的不同在于 multiprocessing 作用于进程,而 dummy 模块作用于线程(因此也包括了 Python 所有常见的多线程限制)。
所以替换使用这两个库异常容易。你可以针对 IO 密集型任务和 CPU 密集型任务来选择不同的库。
动手尝试
使用下面的两行代码来引用包含并行化 map 函数的库:
from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool
实例化 Pool 对象:
pool = ThreadPool()
这条简单的语句替代了 example2.py 中 buildworkerpool 函数 7 行代码的工作。它生成了一系列的 worker 线程并完成初始化工作、将它们储存在变量中以方便访问。
Pool 对象有一些参数,这里我所需要关注的只是它的*个参数:processes. 这一参数用于设定线程池中的线程数。其默认值为当前机器 CPU 的核数。
一般来说,执行 CPU 密集型任务时,调用越多的核速度就越快。但是当处理网络密集型任务时,事情有有些难以预计了,通过实验来确定线程池的大小才是明智的。
pool = ThreadPool(4) # Sets the pool size to 4
线程数过多时,切换线程所消耗的时间甚至会超过实际工作时间。对于不同的工作,通过尝试来找到线程池大小的*优值是个不错的主意。
创建好 Pool 对象后,并行化的程序便呼之欲出了。我们来看看改写后的 example2.py
import urllib2
from multiprocessing.dummy import Pool as ThreadPool
urls = [
    ‘http://www.python.org’,
    ‘http://www.python.org/about/’,
    ‘http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html’,
    ‘http://www.python.org/doc/’,
    ‘http://www.python.org/download/’,
    ‘http://www.python.org/getit/’,
    ‘http://www.python.org/community/’,
    ‘https://wiki.python.org/moin/’,
    ‘http://planet.python.org/’,
    ‘https://wiki.python.org/moin/LocalUserGroups’,
    ‘http://www.python.org/psf/’,
    ‘http://docs.python.org/devguide/’,
    ‘http://www.python.org/community/awards/’
    # etc..
    ]
# Make the Pool of workers
pool = ThreadPool(4)
# Open the urls in their own threads
# and return the results
results = pool.map(urllib2.urlopen, urls)
#close the pool and wait for the work to finish
pool.close()
pool.join()
实际起作用的代码只有 4 行,其中只有一行是关键的。map 函数轻而易举的取代了前文中超过 40 行的例子。为了更有趣一些,我统计了不同方法、不同线程池大小的耗时情况。
# results = []
# for url in urls:
#   result = urllib2.urlopen(url)
#   results.append(result)
# # ——- VERSUS ——- #
# # ——- 4 Pool ——- #
# pool = ThreadPool(4)
# results = pool.map(urllib2.urlopen, urls)
# # ——- 8 Pool ——- #
# pool = ThreadPool(8)
# results = pool.map(urllib2.urlopen, urls)
# # ——- 13 Pool ——- #
# pool = ThreadPool(13)
# results = pool.map(urllib2.urlopen, urls)
结果:
#        Single thread:  14.4 Seconds
#               4 Pool:   3.1 Seconds
#               8 Pool:   1.4 Seconds
#              13 Pool:   1.3 Seconds
很棒的结果不是吗?这一结果也说明了为什么要通过实验来确定线程池的大小。在我的机器上当线程池大小大于 9 带来的收益就十分有限了。
另一个真实的例子
生成上千张图片的缩略图
这是一个 CPU 密集型的任务,并且十分适合进行并行化。
基础单进程版本
import os
import PIL
from multiprocessing import Pool
from PIL import Image
SIZE = (75,75)
SAVE_DIRECTORY = ‘thumbs’
def get_image_paths(folder):
    return (os.path.join(folder, f)
            for f in os.listdir(folder)
            if ‘jpeg’ in f)
def create_thumbnail(filename):
    im = Image.open(filename)
    im.thumbnail(SIZE, Image.ANTIALIAS)
    base, fname = os.path.split(filename)
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
    im.save(save_path)
if __name__ == ‘__main__’:
    folder = os.path.abspath(
        ’11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840′)
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
    images = get_image_paths(folder)
    for image in images:
        create_thumbnail(Image)
上边这段代码的主要工作就是将遍历传入的文件夹中的图片文件,一一生成缩略图,并将这些缩略图保存到特定文件夹中。
这我的机器上,用这一程序处理 6000 张图片需要花费 27.9 秒。
如果我们使用 map 函数来代替 for 循环:
import os
import PIL
from multiprocessing import Pool
from PIL import Image
SIZE = (75,75)
SAVE_DIRECTORY = ‘thumbs’
def get_image_paths(folder):
    return (os.path.join(folder, f)
            for f in os.listdir(folder)
            if ‘jpeg’ in f)
def create_thumbnail(filename):
    im = Image.open(filename)
    im.thumbnail(SIZE, Image.ANTIALIAS)
    base, fname = os.path.split(filename)
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
    im.save(save_path)
if __name__ == ‘__main__’:
    folder = os.path.abspath(
        ’11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840′)
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
    images = get_image_paths(folder)
    pool = Pool()
    pool.map(creat_thumbnail, images)
    pool.close()
    pool.join()
5.6 秒!
虽然只改动了几行代码,我们却明显提高了程序的执行速度。在生产环境中,我们可以为 CPU 密集型任务和 IO 密集型任务分别选择多进程和多线程库来进一步提高执行速度——这也是解决死锁问题的良方。此外,由于 map 函数并不支持手动线程管理,反而使得相关的 debug 工作也变得异常简单。
到这里,我们就实现了(基本)通过一行 Python 实现并行化。
这种方式对于代码的优化侵入较小,这也可以避免在重构代码时发生意外!

测试的价值和目的

测试的价值和目的
这个角色类似于软件开发-测试工程师(SDET),但是有更大的责任来分享他们所引入的学科的知识和热情。
虽然SDET往往一次驻留在一个团队中,但是测试教练的角色可以跨多个团队。
测试教练在原则上类似于Scrum大师或敏捷教练,但不一定是专家。他们只是拥护这一事业的人,如果需要,在底层工作以帮助在开发人员之间灌输文化变化,与产品所有者、领域专家和管理人员一起工作,以帮助将故事扩展到“轻度结构化”的场景中,并在开发工作和测试编写方面进行协作。
测试教练还会听取每个团队的意见,并在测试无效时调整测试过程。
因此,作为一个带着“山”隐喻的测试教练,你的日常生活可以包括以下任何一种活动:
遵循BDD,或至少类似于BDD的流程,并编写自动化验收测试—Cucumber、Fitnesse、Gauge等。
编写和重构代码,以及单元测试和组件测试
鼓励三个amigos风格的会议,其中BA、开发人员和测试人员将用户故事扩展到它的细节—场景/示例,等等;每个涉众都提供了他们对问题的独特观点
倾听和学习——项目中的其他人也有技能和经验;利用这一点来提高自己,同时也使过程适应于人和项目本身。
例如,我在11:FS中发现了这一点,他们已经接受了一个反应性的、基于cqrs的微服务体系结构。我*初的目标是为所有业务场景(包括不愉快的路径)引入e2e测试,但这会使验收测试过于笨拙、运行缓慢,甚至很脆弱,因为单个微服务部署可能会破坏一些不相关的测试。相反,我们保留了满意的路径场景,并将大多数不满意的路径作为组件级测试编写在每个微服务中。结果是良好的全面的需求覆盖,以及仍然快速完成的测试套件。
换句话说,我们避免了臭名昭著的“反向测试金字塔”反模式……实际上,我们*终得到了更多的测试钻石,大多数测试在服务级别进行。这可能不是每个项目的理想,但在这个特殊的情况下,它是理想的。
促使开发人员以面向领域的方式编写测试:即测试业务行为,而不是测试代码的内部结构
这方面有一个很好的例子:*近我一直在与Parallel Agile (PA)密切合作,这是一家与南加州大学(University of South California, USC)关系密切的公司。PA的主要产品是CodeBot,一款基于云的企业代码生成器。实际上,CodeBot更像是一个可执行的架构生成器,因为它从UML域模型创建并部署了一个完整的服务器平台。这样一个“规则密集”的项目需要一套全面的测试,尤其是在产品继续使用新功能进行扩展的情况下。
这些测试包括比我通常编写的更多的低级单元测试,因为底层生成器“引擎”非常复杂。但是,我们通过从许多业务和技术领域提供it示例类模型,使单元测试集中于业务领域。当然,这种方法也可以扩展到更高级别的服务测试。
促使BAs和产品所有者规划业务领域,并确保每个故事都已扩展到足够详细的内容,以便开发人员进行分析,并编写验收测试场景。要做好这一点,需要探索不快乐的路径,而不是快乐的路径。人们喜欢关注目标(有趣的东西,真的),需要经常提醒他们也要考虑边缘情况,以及当事情出错时他们想要发生什么——或者只是发生了不同的事情。
在其他好处中,这确实有助于改进sprint评估。几年前,在一个组织中,我们注意到一个趋势:sprint燃尽图——显示当前两周sprint剩余的估计工作量——会在*周很好地“燃尽”,然后在第二周转向并再次燃尽。剩余的工作量常常比开始的时候还要多!
只有当我们开始将每个故事扩展到测试场景并详细分析这些场景时,问题才变得明显起来。团队是基于不完整的知识来评估每个故事的……我们一直在做“愉快的路径评估”,这意味着每个故事中可能有90%的功能——错误处理、以业务为中心的替代课程等等——都缺失了。这个问题非常普遍,经常导致对任务的低估和成本超支;但这很容易解决。
这表明,面向领域的测试方法可以对项目的整体健康状况产生积*的影响,而不仅仅是对代码和测试的状态。
将测试人员(或测试中的开发人员)推向一个特定的方向,这样他们的验收测试就可以直接从故事场景中驱动,并且同样地探索不愉快的路径,而不是愉快的路径。
当然,这是一个很大的推动。这甚至看起来像是强迫,但实际情况远非如此。就像一寸一寸地移动一面巨大的镜子,你在慢慢地引导团队的参照系,直到你的信息突然到位。这也有助于涉众自己得出正确的结论,从而进一步加强结果。
当然,你不能自己移动镜子;这就是为什么人们的支持是如此重要。利益相关者将会认同过程的原则和目标,如果他们能看到这将使他们的生活更容易,并产生更高质量的产品。
总的来说,测试教练是一个要求高、技能高的角色。您必须很好地掌握您正在“推动”涉众的所有规程。你必须有很好的人际交往技巧,或者至少有一种展示事物的技巧,这样人们就会意识到你是站在他们一边的,和他们一起工作。
例如,在一家公司,我正在展示我们合作的新的自动化测试策略,并且有传言(特别是来自高层管理人员的传言)说这听起来“太像QA了”。公司想要代码质量和测试,但是他们坚决不希望有一个“QA部门”。挖了很久才找到原因;结果证明,解决方案更多的是战略命名,而不是做任何巨大的改变。
“我们需要测试人员,但不要叫他们QA”
在这种敏捷的环境下,对于许多组织来说,QA已经变成了一个肮脏的词汇。然而,对许多人来说,QA现在已经成为瀑布式、大爆炸式集成、流程超负荷、需要填写很长的表单、以及与开发人员分离的部门的同义词,这是不公平的,它提倡软件交付采用“将QA抛给测试人员”的方法。
但是让我们诚实地说,测试教练的目的与QA的目的非常相似:引入并维护一个使团队关注软件质量的过程。
但他们的做法是不同的。像SDET一样,测试教练在每个开发团队中工作,参与他们的日常活动。职责之间仍然存在明显的分离,但是对于任何潜在的问题、业务领域和实际交付的内容,仍然有一个共同的理解。
测试指导鼓励高度集成的设置,产品所有者、测试人员和开发人员一起工作。
因此,即使测试教练完成了与QA相同的一组目标,他们也提供了一种方法,将面向质量的文化变更引入“受敏捷影响”的组织,同时在实践和原则的范围内操作,正是这些实践和原则使得敏捷如此流行。也许这是后敏捷主义?

Python中 and 和 or 运算短路逻辑

Python中 and 和 or 运算短路逻辑
Python入门教程100天
专栏收录该内容
129 篇文章6 订阅
订阅专栏
短路逻辑规则如下:
表达式从左至右运算,若 or 的左侧逻辑值为 True ,则短路 or 后所有的表达式(不管是 and 还是 or),直接输出 or 左侧表达式 。若 or 的左侧逻辑值为 False ,则输出or右侧的表达式,不论其后表达式是真是假,整个表达式结果即为其后表达式的结果。
表达式从左至右运算,若 and 的左侧逻辑值为 False ,则短路其后所有 and 表达式,直到有 or 出现,输出 and 左侧表达式到 or 的左侧,参与接下来的逻辑运算。若 and 的左侧逻辑值为 True,则输出其后的表达式,不论其后表达式是真是假,整个表达式结果即为其后表达式的结果
若 or 的左侧为 False ,或者 and 的左侧为 True 则不能使用短路逻辑。
注意:
1、在Python中and的优先级是大于or的,而且and和or都是会返回值的并且不转换为True和False。当not和and及or在一起运算时,优先级为是not>and>or
2、在Python中,None、任何数值类型中的0、空字符串“”、空元组()、空列表[]、空字典{}都被当作False,还有自定义类型,如果实现了  __ nonzero __ () 或 __ len __ () 方法且方法返回 0 或False,则其实例也被当作False,其他对象均为True。
下面是*简单的逻辑运算:
        True  and True    ==> True                                     True  or True    ==> True
        True  and False   ==> False                                   True  or False   ==> True
        False and True    ==> False                                   False or True    ==> True
        False and False   ==> False                                   False or False   ==> False
接下来我们再举一个具体的例子:
先来定义一组函数:
1>  def a():
    2>     print (‘A’)
    3>     return 1
    4>  def b():
    5>     print (‘B’)
    6>     return 1
    7>  def c():
    8>     print (‘C’)
    9>     return []
    10> def d():
    11>    print (‘D’)
    12>    return []
    13> def e():
    14>    print (‘E’)
    15>    return 1
例1:
17> if a() and b() and c() and d() and e():
    18>    print (‘ok’)
    #显示结果如下
    A
a() 为假 ,其后均为 and 语句,全部短路,*终只返回 a() 的表达式。记住,所有被短路的表达式均不会被输出。所以,此处仅仅打印 A
例2:
17> if a() and b() and c() and d() and e():
    18>    print (‘ok’)
    #显示结果如下
    A
    B
    C
python 从左至右先执行 a() ,a() 返回的逻辑值为 True,后面是 and 语句,所以不能短路其后,继续与 b() 进行逻辑运算,a() and b() 输出 b() 的逻辑值 True,接着与 c() 进行逻辑运算,b() and c() 输出 c() 的逻辑值 False,而其后均为 and 语句, 则全部短路,*终只打印了 A B C 。
例3:
17> if a() or b() or c() or d() or e():
    18>    print (‘ok’)
    #显示结果如下
    A
    Ok
    #显示结果如下
    A
    ok
a() 的逻辑值为 True ,其后均为 or 语句,全部短路,*终只打印了 A,而 if 语句为 True ,所以还要打印一个 ok。
例4:
    17> if a() or b() or c() or d() or e():
    18>    print (‘ok’)
    #显示结果如下
    A
    B
    C
    Ok
python 从左至右先执行 a() ,a() 返回的逻辑值为 False,后面是 or 语句,所以不能短路其后,继续与 b() 进行逻辑运算,a() or b() 输出 b() 的逻辑值 False,接着与 c() 进行逻辑运算,b() or c() 输出 c() 的逻辑值 True,而其后为 or 语句, 则全部短路,*终只打印了 A B C ok。
例5:
 26> if a() and b() and  c() and d() or e() and f() or g() and h():
    27>    print (‘ok’)
    #输出结果如下:
    A
    E
    F
    Ok
别以为语句很长就很难,我们好好分析一下,从左至右,首先a() 的逻辑值为 False,其后到 or 语句为止有三个 and 语句: a() and b() and c() and d(),均被短路。只输出 a(), 得到 a() or e() 为True,输出 e() ,得 e() and F() 为 True ,输出 f(), 其后接 or 语句,则短路其后所有。*终只打印了A E F ok 。

Nginx 的请求处理流程你了解多少

之前我们已经讲解了 Nginx 的基础内容,接下来我们开始介绍 Nginx 的架构基础。
为什么我们要讨论 Nginx 的架构基础?
因为 Nginx 运行在企业内网的*外层也就是边缘节点,那么他处理的的流量是其他应用服务器处理流量的数倍,甚至几个数量级,我们知道任何一种问题在不同的数量级下,他的解决方案是完全不同的,所以在 Nginx 它所处理的应用场景中,所有的问题都会被放大,所以我们必须要去理解,为什么 Nginx 采用 master-worker 这样的一种架构模型,为什么 worker 进程的数量要和 CPU 的核数相匹配?当我们需要在多个 worker 进程之间共享数据的时候,为什么在 TLS 或者说限流、限速这样的场景,他们的共享方式是有所不同的,那么这些都需要我们对 Nginx 的架构有一个清晰的了解。
下面我们先来看一下 Nginx 的请求处理流程。
为什么要去看 Nginx 中的请求处理流程呢?因为其实在之前中我们了解到 Nginx 会记录 access 日志和 error 日志,也可以处理静态的资源,那么也可以做反向代理,那么这些东西我们从 Nginx 内部去看他究竟是怎样处理这些请求,它包含一些什么样的组成部分呢?
Nginx 的请求处理流程
%title插图%num
我们从这张图的*左边来看,*左边在 WEB、EMAIL 和 TCP,也就是说大致有三种流量从这里进入 Nginx 以后,我们 Nginx 中有三个大的状态机,一个是处理 TCP/UDP 的 4 层的传输层状态机和处理应用层的 HTTP 状态以及处理邮件的 MAIL 状态机。
那么为什么我们叫它状态机呢?是因为 Nginx 核心的这个大绿色的框他是用非阻塞的事件驱动处理引擎就是用我们所熟知的 epoll,那么一旦我们使用这种异步处理引擎以后,通常都是需要用状态机来把这个请求正确的识别和处理。
基于这样的一种事件状态处理机,我们在解析出请求需要访问静态资源的时候,我们看到走左下方的这个箭头,那么它就找到了静态资源,如果我们去做反向代理的时候呢,那么对反向代理的内容,我可以做磁盘缓存,缓存到磁盘上,也在下面左下方这条线,但是我们在处理静态资源的时候,会有一个问题就是当整个内存已经不足以完全的缓存所有的文件和信息的时候,那么像 send File 这样的调用或者 AIO 会退化成阻塞的磁盘调用,所以在这里我们需要有一个线程池来处理,对于每一个处理完成的请求呢,我们会进入 access 日志或 error 日志。
那么这里也是进入了磁盘中的,当然我们可以通过 syslog 协议把它进入到远程的机器上,那么更多的时候我们的 Nginx 是作为负载均衡或者反向代理来使用的,就是我们可以把请求通过协议级(HTTP,Mail 及 stream(TCP))传输到后面的服务器,也可以通过例如应用层的一些协议(FastCGI、uWSGI、SCGI、memcached)代理到相应的应用服务器。以上就是 Nginx 的请求处理流程。

在Python中如何实现单例模式。

在Python中如何实现单例模式。
点评:这个题目在面试中出现的频率*高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性*强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
from functools import wraps
def singleton(cls):
    “””单例类装饰器”””
    instances = {}
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper
@singleton
class President:
    pass
扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有必然联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、代理、上下文环境等服务,它是对设计模式中代理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper函数)通常都会用functools模块中的wraps再加以装饰,这个装饰器*重要的作用是给被装饰的类或函数动态添加一个__wrapped__属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不需要装饰功能的时候,可以通过它来取消装饰器,例如可以使用President = President.__wrapped__来取消对President类做的单例处理。需要提醒大家的是:上面的单例并不是线程安全的,如果要做到线程安全,需要对创建对象的代码进行加锁的处理。在Python中可以使用threading模块的RLock对象来提供锁,可以使用锁对象的acquire和release方法来实现加锁和解锁的操作。当然,更为简便的做法是使用锁对象的with上下文语法来进行隐式的加锁和解锁操作。
方法二:使用元类实现单例模式。
class SingletonMeta(type):
    “””自定义单例元类”””
    def __init__(cls, *args, **kwargs):
        cls.__instance = None
        super().__init__(*args, **kwargs)
    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
        return cls.__instance
class President(metaclass=SingletonMeta):
    pass
扩展:Python是面向对象的编程语言,在面向对象的世界中,一切皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在定义类时,如果没有给一个类指定父类,那么默认的父类是object,如果没有给一个类指定元类,那么默认的元类是type。通过自定义的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__魔术方法,改变了President类的构造器那样。
关于单例模式,在面试中还有可能被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。
题目002:不使用中间变量,交换两个变量a和b的值。
点评:典型的送人头的题目,在其他编程语言中不使用中间变量交换两个变量的值可以使用异或运算,Python中还可以通过内置的字节码指令直接交换两个变量的值。
方法一:
a = a ^ b
b = a ^ b
a = a ^ b
方法二:
a, b = b, a
扩展:需要注意,a, b = b, a这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO指令来支持这个操作,类似的还有ROT_THREE,对于3个以上的元素,如a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis模块的dis函数来反汇编你的Python代码。
题目003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。
点评:这个题目在初中级Python岗位面试的时候经常出现,题目源于《Python Cookbook》这本书*章的第10个问题,有很多面试题其实都是这本书上的原题,所以建议大家有时间的话好好研读一下这本书。
def dedup(items):
    no_dup_items = []
    seen = set()
    for item in items:
        if item not in seen:
            no_dup_items.append(item)
            seen.add(item)
    return no_dup_items
当然,也可以像《Python Cookbook》书上的代码那样,把上面的函数改造成一个生成器。
def dedup(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)
扩展:由于Python中的集合底层使用哈希存储,所以集合的in和not in成员运算在性能上远远优于列表,所以上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable对象,因此上面的代码在列表元素不是hashable对象时会失效,要解决这个问题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable对象的函数。
题目004:假设你使用的是官方的CPython,说出下面代码的运行结果。
点评:下面的程序对实际开发并没有什么意义,但却是CPython中的一个大坑,这道题旨在考察面试者对官方的Python解释器到底了解到什么程度。
a, b, c, d = 1, 1, 1000, 1000
print(a is b, c is d)
def foo():
    e = 1000
    f = 1000
    print(e is f, e is d)
    g = 1
    print(g is a)
foo()
结果:
True False
True False
True
上面代码中a is b的结果是True但c is d的结果是False,这一点的确让人费解。这个结果是因为CPython出于性能优化的考虑,把频繁使用的整数对象用一个叫small_ints的对象池缓存起来造成的。small_ints缓存的整数值被设定为[-5, 256]这个区间,也就是说,如果使用CPython解释器,在任何引用这些整数的地方,都不需要重新创建int对象,而是直接引用缓存池中的对象。如果整数不在该范围内,那么即便两个整数的值相同,它们也是不同的对象。
CPython底层为了进一步提升性能还做了一个设定:对于同一个代码块中值不在small_ints缓存范围之内的整数,如果同一个代码块中已经存在一个值与其相同的整数对象,那么就直接引用该对象,否则创建新的int对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点可以自行证明。
扩展:如果你用PyPy(另一种Python解释器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。
题目005:Lambda函数是什么,举例说明的它的应用场景。
点评:这个题目主要想考察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。因为Lambda函数通常用在高阶函数中,主要的作用是通过传入或返回函数实现代码的解耦合。
Lambda函数也叫匿名函数,它功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写return关键字。Lambda函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。
面试的时候有可能还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现题目要求的功能,例如:用一行代码实现求阶乘的函数,用一行代码实现求*大公约数的函数等。
fac = lambda x: __import__(‘functools’).reduce(int.__mul__, range(1, x + 1), 1)
gcd = lambda x, y: y % x and gcd(y % x, x) or x
Lambda函数其实*为主要的用途是把一个函数传入另一个高阶函数(如Python内置的filter、map等)中来为函数做解耦合,增强函数的灵活性和通用性。下面的例子通过使用filter和map函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这filter和map函数没有跟特定的过滤和映射数据的规则耦合在一起。
items = [12, 5, 7, 10, 8, 19]
items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
print(items)    # [25, 49, 361]
当然,用列表的生成式来实现上面的代码更加简单明了,如下所示。
items = [12, 5, 7, 10, 8, 19]
items = [x ** 2 for x in items if x % 2]
print(items)    # [25, 49, 361]
温馨提示:Python面试宝典会持续更新,从基础到项目实战的内容都会慢慢覆盖到。希望伙伴们脚踏实地,一步步提升!感谢同学的支持!

说说Python中的浅拷贝和深拷贝。

说说Python中的浅拷贝和深拷贝。
点评:这个题目本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种面试题,在回答的时候一定要让你的答案能够超出面试官的预期,这样才能获得更好的印象分。所以回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝,*后还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。深拷贝可能会遇到两个问题:一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。Python通过copy模块中的copy和deepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。
deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,显然我们可以使用pickle模块的dumps和loads来做到,代码如下所示。
import pickle
my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
列表的切片操作[:]相当于实现了列表对象的浅拷贝,而字典的copy方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,首先是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”的对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法其实更加高效,这也就是设计模式中的原型模式。我们可以通过元类的方式来实现原型模式,代码如下所示。
import copy
class PrototypeMeta(type):
    “””实现原型模式的元类”””
    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为对象绑定clone方法来实现对象拷贝
        cls.clone = lambda self, is_deep=True: \
            copy.deepcopy(self) if is_deep else copy.copy(self)
class Person(metaclass=PrototypeMeta):
    pass
p1 = Person()
p2 = p1.clone()                 # 深拷贝
p3 = p1.clone(is_deep=False)    # 浅拷贝
题目007:Python是如何实现内存管理的?
点评:当面试官问到这个问题的时候,一个展示自己的机会就摆在面前了。你要先反问面试官:“你说的是官方的CPython解释器吗?”。这个反问可以展示出你了解过Python解释器的不同的实现版本,而且你也知道面试官想问的是CPython。当然,很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以,千万不要觉得面试官一定比你强,怀揣着这份自信可以让你更好的完成面试。
Python提供了自动化的内存管理,也就是说内存空间的分配与释放都是由Python解释器在运行时自动进行的,自动管理内存功能*大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
引用计数:对于CPython解释器来说,Python中的每一个对象其实就是PyObject结构体,它的内部有一个名为ob_refcnt 的引用计数器成员变量。程序在运行的过程中ob_refcnt的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被释放掉。
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
以下情况会导致引用计数加1:
对象被创建
对象被引用
对象作为参数传入到一个函数中
对象作为元素存储到一个容器中
以下情况会导致引用计数减1:
用del语句显示删除对象引用
对象引用被重新赋值其他对象
一个对象离开它所在的作用域
持有该对象的容器自身被销毁
持有该对象的容器删除该对象
可以通过sys模块的getrefcount函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此需要其他的垃圾回收算法对其进行补充。
标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着需要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count变量外,还有一个gc_ref变量,这个gc_ref是ref_count的一个副本,所以初始值为ref_count的大小。执行垃圾回收时,首先遍历链表A中的节点,并且将当前对象所引用的所有对象的gc_ref减1,这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,如果节点的gc_ref值为0,那么这个对象就被标记为“暂时不可达” (
GC_TENTATIVELY_UNREACHABLE) 并被移动到链表B中;如果节点的gc_ref不为0,那么这个对象就会被标记为“可达“ (GC_REACHABLE),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是需要释放内存的节点。
分代回收:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0、1、2,每一个新生对象都在第0代中,如果该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1代中,存在于第1代的对象将较少的被垃圾回收扫描到;如果在对第1代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc模块的get_threshold函数来获得,该函数返回一个三元组,分别表示多少次内存分配操作后会执行0代垃圾回收,多少次0代垃圾回收后会执行1代垃圾回收,多少次1代垃圾回收后会执行2代垃圾回收。需要说明的是,如果执行一次2代垃圾回收,那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值,可以通过gc模块的set_threshold函数来做到。
题目008:说一下你对Python中迭代器和生成器的理解。
点评:很多人面试者都会写迭代器和生成器,但是却无法准确的解释什么是迭代器和生成器。如果你也有同样的困惑,可以参考下面的回答。
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于定义协议或表示约定的关键字,像interface、protocol这些单词并不在Python语言的关键字列表中。Python语言通过魔法方法来表示约定,也就是我们所说的协议,而__next__和__iter__这两个魔法方法就代表了迭代器协议。生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。
面试中经常会让面试者写生成斐波那契数列的迭代器,下面给出参考代码,其他的迭代器可以如法炮制。
class Fib(object):
    def __init__(self, num):
        self.num = num
        self.a, self.b = 0, 1
        self.idx = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.idx < self.num:
            self.a, self.b = self.b, self.a + self.b
            self.idx += 1
            return self.a
        raise StopIteration()
如果用生成器的语法来改写上面的代码,代码会简单优雅很多。
def fib(num):
    a, b = 0, 1
    for _ in range(num):
        a, b = b, a + b
        yield a
可以通过for-in循环从迭代器对象中取出值,也可以使用next函数取出迭代器对象中的下一个值。
题目009:正则表达式的match方法和search方法有什么区别?
点评:正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用re模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过re模块的compile函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。如果一个正则表达式被频繁的使用,我们推荐后面这种方式,它会减少频繁编译同一个正则表达式所造成的开销。
match方法是从字符串的起始位置进行正则表达式匹配,返回Match对象或None。search方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
题目010:下面这段代码的执行结果是什么。
def multiply():
    return [lambda x: i * x for i in range(4)]
print([m(100) for m in multiply()])
运行结果:
[300, 300, 300, 300]
上面代码的运行结果很容易被误判为[0, 100, 200, 300]。首先需要注意的是multiply函数用生成式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i的结果。需要注意的是这里有闭包(closure)现象,multiply函数中的局部变量i的生命周期被延展了,由于i*终的值是3,所以通过m(100)调列表中的Lambda函数时会返回300,而且4个调用都是如此。
如果想得到[0, 100, 200, 300]这个结果,可以按照下面几种方式来修改multiply函数。
方法一:使用生成器,让函数获得i的当前值。
def multiply():
    return (lambda x: i * x for i in range(4))
print([m(100) for m in multiply()])
或者
def multiply():
    for i in range(4):
        yield lambda x: x * i
print([m(100) for m in multiply()])
方法二:使用偏函数,彻底避开闭包现象。
from functools import partial
from operator import __mul__
def multiply():
    return [partial(__mul__, i) for i in range(4)]
print([m(100) for m in multiply()])

Python中为什么没有函数重载?

Python中为什么没有函数重载?
点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。
首先Python是解释型语言,函数重载现象通常出现在编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。
题目012:用Python代码实现Python内置函数max。
点评:这个题目看似简单,但实际上还是比较考察面试者的功底。因为Python内置的max函数既可以传入可迭代对象找出*大,又可以传入两个或多个参数找出*大;*为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。
下面的代码仅供参考:
def my_max(*args, key=None, default=None):
    “””
    获取可迭代对象中*大的元素或两个及以上实参中*大的元素
    :param args: 一个可迭代对象或多个元素
    :param key: 提取用于元素比较的特征值的函数,默认为None
    :param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常
    :return: 返回可迭代对象或多个元素中的*大元素
    “””
    if len(args) == 1 and len(args[0]) == 0:
        if default:
            return default
        else:
            raise ValueError(‘max() arg is an empty sequence’)
    items = args[0] if len(args) == 1 else args
    max_elem, max_value = items[0], items[0]
    if key:
        max_value = key(max_value)
    for item in items:
        value = item
        if key:
            value = key(item)
        if value > max_value:
            max_elem, max_value = item, value
    return max_elem
题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
点评:送人头的题目,不解释。
def count_letters(items):
    result = {}
    for item in items:
        if isinstance(item, (int, float)):
            result[item] = result.get(item, 0) + 1
    return result
也可以直接使用Python标准库中collections模块的Counter类来解决这个问题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。
from collections import Counter
def count_letters(items):
    counter = Counter(items)
    return {key: value for key, value in counter.items() \
            if isinstance(key, (int, float))}
题目014:使用Python代码实现遍历一个文件夹的操作。
Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个生成器。可以通过这个生成器来获得文件夹下所有的文件和文件夹。
import os
g = os.walk(‘/Users/Hao/Downloads/’)
for path, dir_list, file_list in g:
    for dir_name in dir_list:
        print(os.path.join(path, dir_name))
    for file_name in file_list:
        print(os.path.join(path, file_name))
说明:os.path模块提供了很多进行路径操作的工具函数,在项目开发中也是经常会用到的。 如果题目明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
题目015:现有2元、3元、5元共三种面额的货币,如果需要找零99元,一共有多少种找零的方式?
点评:还有一个非常类似的题目:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个题目的思路是一样,如果用递归函数来写的话非常简单。
from functools import lru_cache
@lru_cache()
def change_money(total):
    if total == 0:
        return 1
    if total < 0:
        return 0
    return change_money(total – 2) + change_money(total – 3) + change_money(total – 5

LeakCanary: 让内存泄露无所遁形

LeakCanary: 让内存泄露无所遁形

java.lang.OutOfMemoryError
        at android.graphics.Bitmap.nativeCreate(Bitmap.java:-2)
        at android.graphics.Bitmap.createBitmap(Bitmap.java:689)
        at com.squareup.ui.SignView.createSignatureBitmap(SignView.java:121)

谁也不会喜欢 OutOfMemoryError

在 Square Register 中, 在签名页面,我们把客户的签名画在 bitmap cache 上。 这个 bitmap 的尺寸几乎和屏幕的尺寸一样大,在创建这个 bitmap 对象时,经常会引发 OutOfMemoryError,简称OOM

%title插图%num

当时,我们尝试过一些解决方案,但都没解决问题

  • 使用 Bitmap.Config.ALPHA_8 因为,签名仅有黑色。
  • 捕捉 OutOfMemoryError, 尝试 GC 并重试(受 GCUtils 启发)。
  • 我们没想过在 Java heap 内存之外创建 bitmap 。苦逼的我们,那会 Fresco 还不存在。

路子走错了

其实 bitmap 的尺寸不是真正的问题,当内存吃紧的时候,到处都有可能引发 OO。在创建大对象,比如 bitmap 的时候,更有可能发生。OOM 只是一个表象,更深层次的问题可能是: 内存泄露

什么是内存泄露

一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。

比如,当 Activity.onDestroy 被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这*终将会导致内存耗尽,然后因为 OOM 而 crash。

对战内存泄露

排查内存泄露是一个全手工的过程,这在 Raizlabs 的 Wrangling Dalvik 系列文章中有详细描述。

以下几个关键步骤:

  1. 通过 Bugsnag, Crashlytics 或者 Developer Console 等统计平台,了解 OutOfMemoryError 情况。
  2. 重现问题。为了重现问题,机型非常重要,因为一些问题只在特定的设备上会出现。为了找到特定的机型,你需要想尽一切办法,你可能需要去买,去借,甚至去偷。 当然,为了确定复现步骤,你需要一遍一遍地去尝试。一切都是非常原始和粗暴的。
  3. 在发生内存泄露的时候,把内存 Dump 出来。具体看这里。
  4. 然后,你需要在 MAT 或者 YourKit 之类的内存分析工具中反复查看,找到那些原本该被回收掉的对象。
  5. 计算这个对象到 GC roots 的*短强引用路径。
  6. 确定引用路径中的哪个引用是不该有的,然后修复问题。

很复杂对吧?

如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了,岂不妙哉!

LeakCanary

LeakCanary 是一个检测内存泄露的开源类库。你可以在 debug 包种轻松检测内存泄露。

先看一个例子:

class Cat {
}

class Box {
  Cat hiddenCat;
}
class Docker {
    // 静态变量,将不会被回收,除非加载 Docker 类的 ClassLoader 被回收。
    static Box container;
}

// ...

Box box = new Box();

// 薛定谔之猫
Cat schrodingerCat = new Cat();
box.hiddenCat = schrodingerCat;
Docker.container = box;

创建一个RefWatcher,监控对象引用情况。

// 我们期待薛定谔之猫很快就会消失(或者不消失),我们监控一下
refWatcher.watch(schrodingerCat);

当发现有内存泄露的时候,你会看到一个很漂亮的 leak trace 报告:

  • GC ROOT static Docker.container
  • references Box.hiddenCat
  • leaks Cat instance

我们知道,你很忙,每天都有一大堆需求。所以我们把这个事情弄得很简单,你只需要添加一行代码就行了。然后 LeakCanary 就会自动侦测 activity 的内存泄露了。

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

然后你会在通知栏看到这样很漂亮的一个界面:

%title插图%num

结论

使用 LeakCanary 之后,我们修复了我们 APP 中相当多的内存泄露。我们甚至发现了 Android SDK 中的一些内存泄露问题。

结果是惊艳的,我们减少了 94% 的由 OOM 导致的 crash。

%title插图%num

如果你也想消灭 OOM crash,那还犹豫什么,赶快使用 LeakCanary

LeakCanary-展现Android中的内存泄露

之前碰到的OOM问题,终于很直白的呈现在我的眼前:我尝试了MAT,但是发现不怎么会用。直到今天终于发现了这个新工具:

当我们的App中存在内存泄露时会在通知栏弹出通知:

这里写图片描述

当点击该通知时,会跳转到具体的页面,展示出Leak的引用路径,如下图所示:

这里写图片描述

LeakCanary 可以用更加直白的方式将内存泄露展现在我们的面前。

以下是我找到的学习资料,写的非常棒:
1、LeakCanary: 让内存泄露无所遁形
2、LeakCanary 中文使用说明

AndroidStudio (官方)上使用LeakCanary 请移步:
https://github.com/square/leakcanary

Eclipse 上使用LeakCanary 请移步我的:
https://github.com/SOFTPOWER1991/LeakcanarySample-Eclipse

Android studio (自己弄的)上使用LeakCanary也可以看这个:

leakcanarySample_androidStudio

工程包括:

  1. LeakCanary库代码
  2. LeakCanaryDemo示例代码

使用步骤:

  1. 将LeakCanary import 入自己的工程
  2. 添加依赖:

    compile project(':leakcanary')

  3. 在Application中进行配置
    public class ExampleApplication extends Application {
    
      ......
      //在自己的Application中添加如下代码
    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context
                .getApplicationContext();
        return application.refWatcher;
    }
    
      //在自己的Application中添加如下代码
    private RefWatcher refWatcher;
    
    @Override
    public void onCreate() {
        super.onCreate();
        ......
            //在自己的Application中添加如下代码
        refWatcher = LeakCanary.install(this);
        ......
    }
    
    .....
    }
    
  4. 在Activity中进行配置
public class MainActivity extends AppCompatActivity {

    ......
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
            //在自己的应用初始Activity中加入如下两行代码
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
        refWatcher.watch(this);

        textView = (TextView) findViewById(R.id.tv);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startAsyncTask();
            }
        });

    }

    private void async() {

        startAsyncTask();
    }

    private void startAsyncTask() {
        // This async task is an anonymous class and therefore has a hidden reference to the outer
        // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
        // the activity instance will leak.
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                // Do some slow work in background
                SystemClock.sleep(20000);
                return null;
            }
        }.execute();
    }


}

 

  1. 在AndroidMainfest.xml 中进行配置,添加如下代码
        <service
            android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
            android:enabled="false"
            android:process=":leakcanary" />
        <service
            android:name="com.squareup.leakcanary.DisplayLeakService"
            android:enabled="false" />

        <activity
            android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
            android:enabled="false"
            android:icon="@drawable/__leak_canary_icon"
            android:label="@string/__leak_canary_display_activity_label"
            android:taskAffinity="com.squareup.leakcanary"
            android:theme="@style/__LeakCanary.Base" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

 

IOS初始化控制器的两种方法

题记
生活很简单,只不过就是忘记了知识,再去学习知识。
1
笔录一 ViewControllViewController方式

#import “AppDelegate.h”
#import “ViewController.h”

@interface AppDelegate ()

@property(nonatomic,strong) ViewController *viewController;

@end

@implementation AppDelegate

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

//初始化控制器
self.viewController = [[ViewController alloc]init];
//初始化window
self.window =[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
//设置控制器
self.window.rootViewController = self.viewController;

return YES;
}

笔录二 ViewControllViewController 与 xib方式

#import “AppDelegate.h”

#import “XibViewControllViewController.h”

@interface AppDelegate ()

@property(nonatomic,strong) ViewController *viewController;
@property(nonatomic,strong) XibViewControllViewController *xibControllViewController;

@end

@implementation AppDelegate

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

//初始化控制器
self.xibControllViewController = [[XibViewControllViewController alloc]initWithNibName:@”XibViewControllViewController” bundle:nil];
//初始化window
self.window =[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
//设置控制器
self.window.rootViewController = self.xibControllViewController;

return YES;
}