1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
build / android / method_count.py [blame]
#! /usr/bin/env python3
# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import os
import re
import zipfile
from pylib.dex import dex_parser
class DexStatsCollector:
"""Tracks count of method/field/string/type as well as unique methods."""
def __init__(self):
# Signatures of all methods from all seen dex files.
self._unique_methods = set()
# Map of label -> { metric -> count }.
self._counts_by_label = {}
def _CollectFromDexfile(self, label, dexfile):
assert label not in self._counts_by_label, 'exists: ' + label
self._counts_by_label[label] = {
'fields': dexfile.header.field_ids_size,
'methods': dexfile.header.method_ids_size,
'strings': dexfile.header.string_ids_size,
'types': dexfile.header.type_ids_size,
}
self._unique_methods.update(dexfile.IterMethodSignatureParts())
def CollectFromZip(self, label, path):
"""Add dex stats from an .apk/.jar/.aab/.zip."""
with zipfile.ZipFile(path, 'r') as z:
for subpath in z.namelist():
if not re.match(r'.*classes\d*\.dex$', subpath):
continue
dexfile = dex_parser.DexFile(bytearray(z.read(subpath)))
self._CollectFromDexfile('{}!{}'.format(label, subpath), dexfile)
def CollectFromDex(self, label, path):
"""Add dex stats from a .dex file."""
with open(path, 'rb') as f:
dexfile = dex_parser.DexFile(bytearray(f.read()))
self._CollectFromDexfile(label, dexfile)
def MergeFrom(self, parent_label, other):
"""Add dex stats from another DexStatsCollector."""
# pylint: disable=protected-access
for label, other_counts in other._counts_by_label.items():
new_label = '{}-{}'.format(parent_label, label)
self._counts_by_label[new_label] = other_counts.copy()
self._unique_methods.update(other._unique_methods)
# pylint: enable=protected-access
def GetUniqueMethodCount(self):
"""Returns total number of unique methods across encountered dex files."""
return len(self._unique_methods)
def GetCountsByLabel(self):
"""Returns dict of label -> {metric -> count}."""
return self._counts_by_label
def GetTotalCounts(self):
"""Returns dict of {metric -> count}, where |count| is sum(metric)."""
ret = {}
for metric in ('fields', 'methods', 'strings', 'types'):
ret[metric] = sum(x[metric] for x in self._counts_by_label.values())
return ret
def GetDexCacheSize(self, pre_oreo):
"""Returns number of bytes of dirty RAM is consumed from all dex files."""
# Dex Cache was optimized in Android Oreo:
# https://source.android.com/devices/tech/dalvik/improvements#dex-cache-removal
if pre_oreo:
total = sum(self.GetTotalCounts().values())
else:
total = sum(c['methods'] for c in self._counts_by_label.values())
return total * 4 # 4 bytes per entry.
def main():
parser = argparse.ArgumentParser()
parser.add_argument('paths', nargs='+')
args = parser.parse_args()
collector = DexStatsCollector()
for path in args.paths:
if os.path.splitext(path)[1] in ('.zip', '.apk', '.jar', '.aab'):
collector.CollectFromZip(path, path)
else:
collector.CollectFromDex(path, path)
counts_by_label = collector.GetCountsByLabel()
for label, counts in sorted(counts_by_label.items()):
print('{}:'.format(label))
for metric, count in sorted(counts.items()):
print(' {}:'.format(metric), count)
print()
if len(counts_by_label) > 1:
print('Totals:')
for metric, count in sorted(collector.GetTotalCounts().items()):
print(' {}:'.format(metric), count)
print()
print('Unique Methods:', collector.GetUniqueMethodCount())
print('DexCache (Pre-Oreo):', collector.GetDexCacheSize(pre_oreo=True),
'bytes of dirty memory')
print('DexCache (Oreo+):', collector.GetDexCacheSize(pre_oreo=False),
'bytes of dirty memory')
if __name__ == '__main__':
main()