asset_shield 0.1.5 copy "asset_shield: ^0.1.5" to clipboard
asset_shield: ^0.1.5 copied to clipboard

Native asset encryption and compression for Flutter.

example/lib/main.dart

import 'dart:io';

import 'package:asset_shield/asset_shield.dart';
import 'package:asset_shield/crypto.dart';
import 'package:asset_shield_example/generated/asset_shield_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  Uint8List? key;
  try {
    if (assetShieldKeyBase64.isNotEmpty) {
      key = ShieldKey.fromBase64(assetShieldKeyBase64);
    }
  } catch (_) {
    key = null;
  }

  if (key == null) {
    runApp(const MissingConfigApp());
    return;
  }

  Shield.initialize(
    key: key,
    encryptedAssetsDir: assetShieldEncryptedDir,
  );
  runApp(DefaultAssetBundle(bundle: ShieldAssetBundle(), child: const MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.light,
        colorSchemeSeed: const Color(0xFF0B5FFF),
      ),
      home: const BenchPage(),
    );
  }
}

class MissingConfigApp extends StatelessWidget {
  const MissingConfigApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              '缺少生成的配置文件或密钥。\n\n'
              '请在 example/ 目录下执行:\n'
              '  dart run asset_shield init\n'
              '  dart run asset_shield encrypt\n\n'
              '然后重启 App。',
            ),
          ),
        ),
      ),
    );
  }
}

enum _BenchMode {
  nativeRead,
  bundleRead,
}

class BenchPage extends StatefulWidget {
  const BenchPage({super.key});

  @override
  State<BenchPage> createState() => _BenchPageState();
}

class _BenchPageState extends State<BenchPage> {
  final _assetPathController =
      TextEditingController(text: 'assets/images/images.jpg');
  final _itersController = TextEditingController(text: '20');

  bool _warmup = true;
  bool _running = false;
  String _log = '';
  _BenchSummary? _lastSummary;

  @override
  void dispose() {
    _assetPathController.dispose();
    _itersController.dispose();
    super.dispose();
  }

  int _normalizeWorkers(int v) {
    if (v < 0) return Platform.numberOfProcessors;
    if (v == 0) return 1;
    return v < 1 ? 1 : v;
  }

  Future<_OneRun> _runOnce({
    required ShieldFfi ffi,
    required Uint8List key,
    required String encryptedPath,
    required _BenchMode mode,
    required int cryptoWorkers,
    required int zstdWorkers,
  }) async {
    int bundleLoadUs = 0;
    int decryptUs = 0;
    int plainBytes = 0;
    int encBytes = 0;

    if (mode == _BenchMode.nativeRead) {
      final sw = Stopwatch()..start();
      final out = ffi.decryptAsset(
        encryptedPath,
        key,
        cryptoWorkers: cryptoWorkers,
        zstdWorkers: zstdWorkers,
      );
      try {
        sw.stop();
        decryptUs = sw.elapsedMicroseconds;
        plainBytes = out.length;
      } finally {
        ffi.release(out);
      }
    } else {
      final swLoad = Stopwatch()..start();
      final data = await rootBundle.load(encryptedPath);
      swLoad.stop();
      bundleLoadUs = swLoad.elapsedMicroseconds;

      final enc =
          data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
      encBytes = enc.length;

      final swDec = Stopwatch()..start();
      final out = ffi.decrypt(
        enc,
        key,
        cryptoWorkers: cryptoWorkers,
        zstdWorkers: zstdWorkers,
      );
      try {
        swDec.stop();
        decryptUs = swDec.elapsedMicroseconds;
        plainBytes = out.length;
      } finally {
        ffi.release(out);
      }
    }

    return _OneRun(
      mode: mode,
      bundleLoadUs: bundleLoadUs,
      decryptUs: decryptUs,
      plainBytes: plainBytes,
      encBytes: encBytes,
    );
  }

  Future<void> _runBench(_BenchMode mode) async {
    if (_running) return;

    setState(() {
      _running = true;
      _log = '';
      _lastSummary = null;
    });

    final log = StringBuffer();
    _BenchSummary? summary;
    try {
      final ffi = ShieldFfi.load();
      final key = ShieldKey.fromBase64(assetShieldKeyBase64);

      final iters = int.tryParse(_itersController.text.trim()) ?? 20;
      final cryptoWorkers = _normalizeWorkers(-1);
      final zstdWorkers = _normalizeWorkers(-1);

      final originalPath = _assetPathController.text.trim();
      if (originalPath.isEmpty) {
        log.writeln('资源路径为空。');
        return;
      }

      final encryptedPath = Shield.resolvePath(originalPath);

      log.writeln('测试模式: ${mode == _BenchMode.nativeRead ? '原生读取+解密' : 'Bundle读取+解密'}');
      log.writeln('迭代次数: $iters, 预热: ${_warmup ? '开' : '关'}');
      log.writeln('cryptoWorkers: $cryptoWorkers, zstdWorkers: $zstdWorkers');
      log.writeln('原始路径: $originalPath');
      log.writeln('加密路径: $encryptedPath');
      log.writeln('');

      if (_warmup) {
        log.writeln('预热中...');
        await _runOnce(
          ffi: ffi,
          key: key,
          encryptedPath: encryptedPath,
          mode: mode,
          cryptoWorkers: cryptoWorkers,
          zstdWorkers: zstdWorkers,
        );
        log.writeln('预热完成。');
        log.writeln('');
      }

      summary = _BenchSummary.zero(mode: mode);

      for (var i = 0; i < iters; i++) {
        log.writeln('第 ${i + 1} 次:');
        final r = await _runOnce(
          ffi: ffi,
          key: key,
          encryptedPath: encryptedPath,
          mode: mode,
          cryptoWorkers: cryptoWorkers,
          zstdWorkers: zstdWorkers,
        );
        summary.add(r);

        log.writeln('  bundleLoad: ${(r.bundleLoadUs / 1000).toStringAsFixed(2)} ms');
        log.writeln('  解密耗时: ${(r.decryptUs / 1000).toStringAsFixed(2)} ms');
        log.writeln(
          '  总耗时: ${(r.totalUs / 1000).toStringAsFixed(2)} ms, '
          '吞吐: ${r.throughputMiBs.toStringAsFixed(2)} MiB/s',
        );
        log.writeln('');
        if (!mounted) break;
      }
    } catch (e, st) {
      log.writeln('发生错误: $e');
      log.writeln(st);
    } finally {
      if (!mounted) return;
      setState(() {
        _log = log.toString();
        _lastSummary = summary;
        _running = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final summary = _lastSummary;

    return Scaffold(
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [
              scheme.primary.withValues(alpha: 0.10),
              scheme.secondary.withValues(alpha: 0.08),
              scheme.surface,
            ],
          ),
        ),
        child: SafeArea(
          child: ListView(
            padding: const EdgeInsets.all(16),
            children: [
              _HeaderCard(running: _running),
              const SizedBox(height: 12),
              _ControlsCard(
                running: _running,
                assetPathController: _assetPathController,
                itersController: _itersController,
                warmup: _warmup,
                onWarmupChanged: (v) => setState(() => _warmup = v),
                onRunNative: () => _runBench(_BenchMode.nativeRead),
                onRunBundle: () => _runBench(_BenchMode.bundleRead),
              ),
              const SizedBox(height: 12),
              _ResultCard(summary: summary),
              const SizedBox(height: 12),
              _LogCard(log: _log),
            ],
          ),
        ),
      ),
    );
  }
}

class _HeaderCard extends StatelessWidget {
  const _HeaderCard({required this.running});

  final bool running;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: scheme.outlineVariant),
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            scheme.primary.withValues(alpha: 0.18),
            scheme.secondary.withValues(alpha: 0.10),
          ],
        ),
      ),
      child: Row(
        children: [
          Container(
            width: 44,
            height: 44,
            decoration: BoxDecoration(
              color: scheme.primaryContainer,
              borderRadius: BorderRadius.circular(14),
            ),
            child: Icon(Icons.bolt, color: scheme.onPrimaryContainer),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Asset Shield 性能测试',
                  style: Theme.of(context).textTheme.titleLarge?.copyWith(
                        fontWeight: FontWeight.w800,
                        letterSpacing: 0.2,
                      ),
                ),
                const SizedBox(height: 4),
                Text(
                  running ? '执行中...(建议不要切到后台)' : '点击按钮触发: 读取 -> 解密 -> 解码',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        color: scheme.onSurfaceVariant,
                      ),
                ),
                if (running) ...[
                  const SizedBox(height: 10),
                  const LinearProgressIndicator(),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ControlsCard extends StatelessWidget {
  const _ControlsCard({
    required this.running,
    required this.assetPathController,
    required this.itersController,
    required this.warmup,
    required this.onWarmupChanged,
    required this.onRunNative,
    required this.onRunBundle,
  });

  final bool running;
  final TextEditingController assetPathController;
  final TextEditingController itersController;
  final bool warmup;
  final ValueChanged<bool> onWarmupChanged;
  final VoidCallback onRunNative;
  final VoidCallback onRunBundle;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Card(
      elevation: 0,
      color: scheme.surface,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: scheme.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              '参数设置',
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: assetPathController,
              enabled: !running,
              decoration: const InputDecoration(
                labelText: '原始资源路径(明文路径)',
                hintText: '例如: assets/images/images.jpeg',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: itersController,
                    enabled: !running,
                    keyboardType: TextInputType.number,
                    decoration: const InputDecoration(
                      labelText: '迭代次数',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Switch(
                  value: warmup,
                  onChanged: running ? null : onWarmupChanged,
                ),
                const SizedBox(width: 8),
                // Text('预热(推荐打开)', style: Theme.of(context).textTheme.bodyMedium),
                const Spacer(),
                FilledButton.icon(
                  onPressed: running ? null : onRunNative,
                  icon: const Icon(Icons.memory),
                  label: const Text('原生读取'),
                ),
                const SizedBox(width: 12),
                OutlinedButton.icon(
                  onPressed: running ? null : onRunBundle,
                  icon: const Icon(Icons.folder_open),
                  label: const Text('Bundle读取'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _ResultCard extends StatelessWidget {
  const _ResultCard({required this.summary});

  final _BenchSummary? summary;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Card(
      elevation: 0,
      color: scheme.surface,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: scheme.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              '结果',
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
            ),
            const SizedBox(height: 12),
            if (summary == null)
              Text(
                '说明: 这里统计的是端到端耗时(读取+解密(+解压))。',
                style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      color: scheme.onSurfaceVariant,
                    ),
              )
            else
              _SummaryView(summary: summary!),
          ],
        ),
      ),
    );
  }
}

class _SummaryView extends StatelessWidget {
  const _SummaryView({required this.summary});

  final _BenchSummary summary;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: scheme.outlineVariant),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '汇总(平均值)',
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
                  fontWeight: FontWeight.w700,
                ),
          ),
          const SizedBox(height: 8),
          Text('模式: ${summary.mode == _BenchMode.nativeRead ? '原生读取' : 'Bundle读取'}',
              style: Theme.of(context).textTheme.bodySmall),
          const SizedBox(height: 4),
          Text('总耗时: ${summary.avgTotalMs.toStringAsFixed(2)} ms',
              style: Theme.of(context).textTheme.bodySmall),
          const SizedBox(height: 4),
          Text('吞吐: ${summary.avgThroughputMiBs.toStringAsFixed(2)} MiB/s',
              style: Theme.of(context).textTheme.bodySmall),
          if (summary.mode == _BenchMode.bundleRead) ...[
            const SizedBox(height: 4),
            Text('Bundle读取: ${summary.avgBundleLoadMs.toStringAsFixed(2)} ms',
                style: Theme.of(context).textTheme.bodySmall),
          ],
          const SizedBox(height: 4),
          Text('解密: ${summary.avgDecryptMs.toStringAsFixed(2)} ms',
              style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}

class _LogCard extends StatelessWidget {
  const _LogCard({required this.log});

  final String log;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Card(
      elevation: 0,
      color: scheme.surface,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: scheme.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Text(
                  '运行日志',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.w700,
                      ),
                ),
                const Spacer(),
                IconButton(
                  onPressed:
                      log.isEmpty ? null : () => Clipboard.setData(ClipboardData(text: log)),
                  icon: const Icon(Icons.copy_all),
                  tooltip: '复制日志',
                ),
              ],
            ),
            const SizedBox(height: 8),
            Container(
              height: 260,
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: scheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(12),
              ),
              child: SingleChildScrollView(
                child: Text(
                  log.isEmpty ? '点击按钮开始测试...' : log,
                  style: const TextStyle(
                    fontFamily: 'monospace',
                    fontSize: 12,
                    height: 1.35,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _OneRun {
  _OneRun({
    required this.mode,
    required this.bundleLoadUs,
    required this.decryptUs,
    required this.plainBytes,
    required this.encBytes,
  });

  final _BenchMode mode;
  final int bundleLoadUs;
  final int decryptUs;
  final int plainBytes;
  final int encBytes;

  int get totalUs => bundleLoadUs + decryptUs;

  double get throughputMiBs {
    final sec = totalUs / 1e6;
    final mib = plainBytes / (1024 * 1024);
    if (sec == 0) return 0;
    return mib / sec;
  }
}

class _BenchSummary {
  _BenchSummary._({
    required this.mode,
    required this.runs,
    required this.totalBundleLoadUs,
    required this.totalDecryptUs,
    required this.totalPlainBytes,
  });

  factory _BenchSummary.zero({required _BenchMode mode}) {
    return _BenchSummary._(
      mode: mode,
      runs: 0,
      totalBundleLoadUs: 0,
      totalDecryptUs: 0,
      totalPlainBytes: 0,
    );
  }

  final _BenchMode mode;
  int runs;
  int totalBundleLoadUs;
  int totalDecryptUs;
  int totalPlainBytes;

  void add(_OneRun run) {
    runs += 1;
    totalBundleLoadUs += run.bundleLoadUs;
    totalDecryptUs += run.decryptUs;
    totalPlainBytes += run.plainBytes;
  }

  double get avgBundleLoadMs => runs == 0 ? 0 : totalBundleLoadUs / runs / 1000.0;
  double get avgDecryptMs => runs == 0 ? 0 : totalDecryptUs / runs / 1000.0;

  double get avgTotalMs =>
      runs == 0 ? 0 : (totalBundleLoadUs + totalDecryptUs) / runs / 1000.0;

  double get avgThroughputMiBs {
    final sec = (totalBundleLoadUs + totalDecryptUs) / 1e6;
    final mib = totalPlainBytes / (1024 * 1024);
    if (sec == 0) return 0;
    return mib / sec;
  }
}
0
likes
150
points
0
downloads

Publisher

verified publisherliliin.icu

Weekly Downloads

Native asset encryption and compression for Flutter.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

crypto, ffi, flutter, path, yaml

More

Packages that depend on asset_shield

Packages that implement asset_shield