| # Copyright 2018 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Runs apkanalyzer to parse dex files in an apk. |
| |
| Assumes that apk_path.mapping and apk_path.jar.info is available. |
| """ |
| |
| import logging |
| import os |
| import subprocess |
| import zipfile |
| |
| import models |
| import path_util |
| |
| |
| _TOTAL_NODE_NAME = '<TOTAL>' |
| _DEX_PATH_COMPONENT = 'prebuilt' |
| |
| |
| def _ParseJarInfoFile(file_name): |
| with open(file_name, 'r') as info: |
| source_map = dict() |
| for line in info: |
| package_path, file_path = line.strip().split(',', 1) |
| source_map[package_path] = file_path |
| return source_map |
| |
| |
| def _RunApkAnalyzer(apk_path, mapping_path, output_directory): |
| args = [path_util.GetApkAnalyzerPath(output_directory), 'dex', 'packages', |
| apk_path] |
| if mapping_path and os.path.exists(mapping_path): |
| args.extend(['--proguard-mappings', mapping_path]) |
| output = subprocess.check_output(args) |
| data = [] |
| for line in output.splitlines(): |
| vals = line.split() |
| # We want to name these columns so we know exactly which is which. |
| # pylint: disable=unused-variable |
| node_type, state, defined_methods, referenced_methods, size, name = ( |
| vals[0], vals[1], vals[2], vals[3], vals[4], vals[5:]) |
| data.append((node_type, ' '.join(name), int(size))) |
| return data |
| |
| |
| def _ExpectedDexTotalSize(apk_path): |
| dex_total = 0 |
| with zipfile.ZipFile(apk_path) as z: |
| for zip_info in z.infolist(): |
| if not zip_info.filename.endswith('.dex'): |
| continue |
| dex_total += zip_info.file_size |
| return dex_total |
| |
| |
| # VisibleForTesting |
| def UndoHierarchicalSizing(data): |
| """Subtracts child node sizes from parent nodes. |
| |
| Note that inner classes |
| should be considered as siblings rather than child nodes. |
| |
| Example nodes: |
| [ |
| ('P', '<TOTAL>', 37), |
| ('P', 'org', 32), |
| ('P', 'org.chromium', 32), |
| ('C', 'org.chromium.ClassA', 14), |
| ('M', 'org.chromium.ClassA void methodA()', 10), |
| ('C', 'org.chromium.ClassA$Proxy', 8), |
| ] |
| |
| Processed nodes: |
| [ |
| ('<TOTAL>', 15), |
| ('org.chromium.ClassA', 4), |
| ('org.chromium.ClassA void methodA()', 10), |
| ('org.chromium.ClassA$Proxy', 8), |
| ] |
| """ |
| num_nodes = len(data) |
| nodes = [] |
| |
| def process_node(start_idx): |
| assert start_idx < num_nodes, 'Attempting to parse beyond data array.' |
| node_type, name, size = data[start_idx] |
| total_child_size = 0 |
| next_idx = start_idx + 1 |
| name_len = len(name) |
| while next_idx < num_nodes: |
| next_name = data[next_idx][1] |
| if name == _TOTAL_NODE_NAME or ( |
| len(next_name) > name_len and next_name.startswith(name) |
| and next_name[name_len] in '. '): |
| # Child node |
| child_next_idx, child_node_size = process_node(next_idx) |
| next_idx = child_next_idx |
| total_child_size += child_node_size |
| else: |
| # Sibling or higher nodes |
| break |
| |
| # Apkanalyzer may overcount private method sizes at times. Unfortunately |
| # the fix is not in the version we have in Android SDK Tools. For now we |
| # prefer to undercount child sizes since the parent's size is more |
| # accurate. This means the sum of child nodes may exceed its immediate |
| # parent node's size. |
| total_child_size = min(size, total_child_size) |
| # TODO(wnwen): Add assert back once dexlib2 2.2.5 is released and rolled. |
| #assert total_child_size <= size, ( |
| # 'Child node total size exceeded parent node total size') |
| |
| node_size = size - total_child_size |
| # It is valid to have a package and a class with the same name. |
| # To avoid having two symbols with the same name in these cases, do not |
| # create symbols for packages (which have no size anyways). |
| if node_type == 'P' and node_size != 0 and name != _TOTAL_NODE_NAME: |
| logging.warning('Unexpected java package that takes up size: %d, %s', |
| node_size, name) |
| if node_type != 'P' or node_size != 0: |
| nodes.append((node_type, name, node_size)) |
| return next_idx, size |
| |
| idx = 0 |
| while idx < num_nodes: |
| idx = process_node(idx)[0] |
| return nodes |
| |
| |
| def CreateDexSymbols(apk_path, mapping_path, size_info_prefix, |
| output_directory): |
| source_map = _ParseJarInfoFile(size_info_prefix + '.jar.info') |
| |
| nodes = _RunApkAnalyzer(apk_path, mapping_path, output_directory) |
| nodes = UndoHierarchicalSizing(nodes) |
| |
| dex_expected_size = _ExpectedDexTotalSize(apk_path) |
| total_node_size = sum(map(lambda x: x[2], nodes)) |
| # TODO(agrieve): Figure out why this log is triggering for |
| # ChromeModernPublic.apk (https://crbug.com/851535). |
| # Reporting: dex_expected_size=6546088 total_node_size=6559549 |
| if dex_expected_size < total_node_size: |
| logging.error( |
| 'Node size too large, check for node processing errors. ' |
| 'dex_expected_size=%d total_node_size=%d', dex_expected_size, |
| total_node_size) |
| # We have more than 100KB of ids for methods, strings |
| id_metadata_overhead_size = dex_expected_size - total_node_size |
| symbols = [] |
| for _, name, node_size in nodes: |
| package = name.split(' ', 1)[0] |
| class_path = package.split('$')[0] |
| source_path = source_map.get(class_path, '') |
| if source_path: |
| object_path = package |
| elif package == _TOTAL_NODE_NAME: |
| name = '* Unattributed Dex' |
| object_path = '' # Categorize in the anonymous section. |
| node_size += id_metadata_overhead_size |
| else: |
| object_path = os.path.join(models.APK_PREFIX_PATH, *package.split('.')) |
| if name.endswith(')'): |
| section_name = models.SECTION_DEX_METHOD |
| else: |
| section_name = models.SECTION_DEX |
| symbols.append(models.Symbol( |
| section_name, node_size, full_name=name, object_path=object_path, |
| source_path=source_path)) |
| return symbols |