Add service disposal and lifecycle cleanup

This commit improves memory management, reduces redundant interpreter
instantiation, and standardizes service usage patterns.

- Add `dispose()` to `ExtensionService` interface and implement it across
  Dart, JS, LNReader, and Mihon services.
- Replace repeated interpreter creation in `DartExtensionService` with a
  persistent `_interpreter` instance initialized once in the constructor.
- Add disposal logic for JS DOM selector and Cheerio instances to prevent
  memory leaks.
- Introduce `withExtensionService()` helper to ensure services are always
  disposed after use.
- Update call sites across the codebase to use `withExtensionService()`
  or manual try/finally disposal.
- Improve isolate service message handling by extracting `responsePort`
  earlier.
- Ensure safer defaults (e.g., returning empty lists, const lists) when
  service calls fail.
This commit is contained in:
NBA2K1 2026-01-13 01:11:19 +01:00
parent 267f2d12df
commit 8b1f7fc581
18 changed files with 239 additions and 205 deletions

View file

@ -14,133 +14,105 @@ import '../interface.dart';
class DartExtensionService implements ExtensionService {
@override
late Source source;
D4rt? _interpreter;
DartExtensionService(this.source);
DartExtensionService(this.source) {
_interpreter = D4rt();
RegistrerBridge.registerBridge(_interpreter!);
D4rt _executeLib() {
final interpreter = D4rt();
RegistrerBridge.registerBridge(interpreter);
interpreter.execute(
_interpreter!.execute(
source: source.sourceCode!.replaceAll('Client(source)', 'Client()'),
positionalArgs: [source.toMSource()],
);
return interpreter;
}
@override
void dispose() {
_interpreter = null;
}
@override
Map<String, String> getHeaders() {
Map<String, String> headers = {};
try {
headers = _executeLib().invoke('headers', []) as Map<String, String>;
return _interpreter!.invoke('headers', []) as Map<String, String>;
} catch (_) {
try {
headers =
_executeLib().invoke('getHeader', [source.baseUrl!])
as Map<String, String>;
} catch (_) {}
return _interpreter!.invoke('getHeader', [source.baseUrl!])
as Map<String, String>;
} catch (_) {
return {};
}
}
return headers;
}
@override
String get sourceBaseUrl {
String? baseUrl;
try {
final interpreter = _executeLib();
baseUrl = interpreter.invoke('baseUrl', []) as String?;
} catch (_) {}
return baseUrl == null || baseUrl.isEmpty ? source.baseUrl! : baseUrl;
final baseUrl = _interpreter!.invoke('baseUrl', []) as String?;
return (baseUrl == null || baseUrl.isEmpty) ? source.baseUrl! : baseUrl;
} catch (_) {
return source.baseUrl!;
}
}
@override
bool get supportsLatest {
bool? supportsLatest;
try {
final interpreter = _executeLib();
supportsLatest = interpreter.invoke('supportsLatest', []) as bool?;
} catch (e) {
supportsLatest = true;
return _interpreter!.invoke('supportsLatest', []) as bool? ?? true;
} catch (_) {
return true;
}
return supportsLatest ?? true;
}
@override
Future<MPages> getPopular(int page) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('getPopular', [page]);
return result as MPages;
}
Future<MPages> getPopular(int page) async =>
await _interpreter!.invoke('getPopular', [page]) as MPages;
@override
Future<MPages> getLatestUpdates(int page) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('getLatestUpdates', [page]);
return result as MPages;
}
Future<MPages> getLatestUpdates(int page) async =>
await _interpreter!.invoke('getLatestUpdates', [page]) as MPages;
@override
Future<MPages> search(String query, int page, List<dynamic> filters) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('search', [
query,
page,
FilterList(filters),
]);
return result as MPages;
return await _interpreter!.invoke('search', [
query,
page,
FilterList(filters),
])
as MPages;
}
@override
Future<MManga> getDetail(String url) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('getDetail', [url]);
return result as MManga;
}
Future<MManga> getDetail(String url) async =>
await _interpreter!.invoke('getDetail', [url]) as MManga;
@override
Future<List<PageUrl>> getPageList(String url) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('getPageList', [url]);
return (result as List)
.map(
(e) => e is String
? PageUrl(e.toString().trim())
: PageUrl.fromJson((e as Map).toMapStringDynamic!),
)
.toList();
final result = await _interpreter!.invoke('getPageList', [url]) as List;
return result.map((e) {
if (e is String) return PageUrl(e.trim());
return PageUrl.fromJson((e as Map).toMapStringDynamic!);
}).toList();
}
@override
Future<List<Video>> getVideoList(String url) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('getVideoList', [url]);
return (result as List).cast<Video>();
}
Future<List<Video>> getVideoList(String url) async =>
(await _interpreter!.invoke('getVideoList', [url]) as List).cast<Video>();
@override
Future<String> getHtmlContent(String url, String? referer) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('getHtmlContent', [url, referer]);
return result as String;
}
Future<String> getHtmlContent(String url, String? referer) async =>
await _interpreter!.invoke('getHtmlContent', [url, referer]) as String;
@override
Future<String> cleanHtmlContent(String html) async {
final interpreter = _executeLib();
final result = await interpreter.invoke('cleanHtmlContent', [html]);
return result as String;
}
Future<String> cleanHtmlContent(String html) async =>
await _interpreter!.invoke('cleanHtmlContent', [html]) as String;
@override
FilterList getFilterList() {
List<dynamic> list;
List<dynamic> list = [];
try {
final interpreter = _executeLib();
list = interpreter.invoke('getFilterList', []) as List;
} catch (_) {
list = [];
}
list = _interpreter!.invoke('getFilterList', []) as List;
} catch (_) {}
return FilterList(_toValueList(list));
}
@ -176,11 +148,10 @@ class DartExtensionService implements ExtensionService {
@override
List<SourcePreference> getSourcePreferences() {
try {
final interpreter = _executeLib();
final result = interpreter.invoke('getSourcePreferences', []);
final result = _interpreter!.invoke('getSourcePreferences', []);
return (result as List).cast();
} catch (_) {
return [];
return const [];
}
}
}

View file

@ -15,6 +15,8 @@ abstract interface class ExtensionService {
String get sourceBaseUrl;
bool get supportsLatest;
void dispose();
Map<String, String> getHeaders();
Future<MPages> getPopular(int page);

View file

@ -407,4 +407,10 @@ class Element {
}
''');
}
void dispose() {
if (_elements.isEmpty) return;
_elements.clear();
_elementKey = 0;
}
}

View file

@ -21,6 +21,7 @@ class JsExtensionService implements ExtensionService {
@override
late Source source;
bool _isInitialized = false;
late JsDomSelector _jsDomSelector;
JsExtensionService(this.source);
@ -28,9 +29,9 @@ class JsExtensionService implements ExtensionService {
if (_isInitialized) return;
runtime = getJavascriptRuntime();
JsHttpClient(runtime).init();
JsDomSelector(runtime).init();
JsVideosExtractors(runtime).init();
_jsDomSelector = JsDomSelector(runtime)..init();
JsUtils(runtime).init();
JsVideosExtractors(runtime).init();
JsPreferences(runtime, source).init();
runtime.evaluate('''
@ -85,6 +86,13 @@ var extention = new DefaultExtension();
_isInitialized = true;
}
@override
void dispose() {
if (!_isInitialized) return;
_jsDomSelector.dispose();
_isInitialized = false;
}
@override
Map<String, String> getHeaders() {
return _extensionCall<Map>(

View file

@ -14,3 +14,16 @@ ExtensionService getExtensionService(Source source, String androidProxyServer) {
SourceCodeLanguage.lnreader => LNReaderExtensionService(source),
};
}
Future<T> withExtensionService<T>(
Source source,
String proxyServer,
Future<T> Function(ExtensionService service) action,
) async {
final service = getExtensionService(source, proxyServer);
try {
return await action(service);
} finally {
service.dispose();
}
}

View file

@ -411,4 +411,10 @@ function load(html) {
}
''');
}
void dispose() {
if (_elements.isEmpty) return;
_elements.clear();
_elementKey = 0;
}
}

View file

@ -32,6 +32,7 @@ class LNReaderExtensionService implements ExtensionService {
@override
late Source source;
bool _isInitialized = false;
late JsCheerio _jsCheerio;
LNReaderExtensionService(this.source);
@ -45,7 +46,7 @@ module={},exports=Function("return this")(),Object.defineProperties(module,{name
JsHttpClient(runtime).init();
JsLibs(runtime).init();
JsHtmlParser(runtime).init();
JsCheerio(runtime).init();
_jsCheerio = JsCheerio(runtime)..init();
runtime.evaluate('''
const require = (package) => {
switch (package) {
@ -88,6 +89,13 @@ const extension = exports.default;
_isInitialized = true;
}
@override
void dispose() {
if (!_isInitialized) return;
_jsCheerio.dispose();
_isInitialized = false;
}
@override
Map<String, String> getHeaders() {
return {};

View file

@ -22,11 +22,12 @@ class MihonExtensionService implements ExtensionService {
late String androidProxyServer;
@override
late Source source;
late InterceptedClient client;
late final InterceptedClient client = MClient.init();
MihonExtensionService(this.source, this.androidProxyServer) {
client = MClient.init();
}
MihonExtensionService(this.source, this.androidProxyServer);
@override
void dispose() {}
@override
Map<String, String> getHeaders() {

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:json_view/json_view.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/eval/interface.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart';
@ -487,38 +488,33 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
final proxyServer = ref.read(
androidProxyServerStateProvider,
);
final service = getExtensionService(
source!,
proxyServer,
);
try {
Future<dynamic> Function(
ExtensionService service,
)?
serviceFunc;
if (_serviceIndex == 0) {
final getManga =
await getIsolateService.get<
MPages?
>(
page: _page,
source: source,
serviceType: 'getPopular',
proxyServer: ref.read(
androidProxyServerStateProvider,
),
useLogger: true,
);
await getIsolateService
.get<MPages?>(
page: _page,
source: source,
serviceType: 'getPopular',
proxyServer: proxyServer,
useLogger: true,
);
result = getManga!.toJson();
} else if (_serviceIndex == 1) {
final getManga =
await getIsolateService.get<
MPages?
>(
page: _page,
source: source,
serviceType: 'getLatestUpdates',
proxyServer: ref.read(
androidProxyServerStateProvider,
),
useLogger: true,
);
await getIsolateService
.get<MPages?>(
page: _page,
source: source,
serviceType:
'getLatestUpdates',
proxyServer: proxyServer,
useLogger: true,
);
result = getManga!.toJson();
} else if (_serviceIndex == 2) {
final getManga =
@ -543,24 +539,43 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
proxyServer: proxyServer,
useLogger: true,
);
result = getManga.toJson();
} else if (_serviceIndex == 4) {
result = {
"pages": (await service.getPageList(
_url,
)).map((e) => e.toJson()).toList(),
serviceFunc = (service) async {
return {
"pages":
(await service.getPageList(
_url,
))
.map((e) => e.toJson())
.toList(),
};
};
} else if (_serviceIndex == 5) {
result = (await service.getVideoList(
_url,
)).map((e) => e.toJson()).toList();
serviceFunc = (service) async {
return (await service.getVideoList(
_url,
)).map((e) => e.toJson()).toList();
};
} else if (_serviceIndex == 6) {
result = (await service
.getHtmlContent("test", _url));
serviceFunc = (service) async {
return await service.getHtmlContent(
"test",
_url,
);
};
} else {
result = (await service
.cleanHtmlContent(_html));
serviceFunc = (service) async {
return await service
.cleanHtmlContent(_html);
};
}
if (serviceFunc != null) {
result = await withExtensionService(
source!,
proxyServer,
serviceFunc,
);
}
if (context.mounted) {
setState(() {

View file

@ -355,10 +355,12 @@ Future<void> downloadChapter(
if (!file.existsSync() && novelPage != null) {
final source = getSource(manga.lang!, manga.source!, manga.sourceId)!;
p.join(chapterDirectory.path, "$chapterName.html");
final html = await getExtensionService(
final html = await withExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).getHtmlContent(chapter.manga.value!.name!, chapter.url!);
(service) =>
service.getHtmlContent(chapter.manga.value!.name!, chapter.url!),
);
if (html.isNotEmpty) {
await file.writeAsString(html);
await setProgress(

View file

@ -191,10 +191,15 @@ Future<void> _updateSource(
androidProxyServer,
);
} else {
headers = getExtensionService(
final service = getExtensionService(
source..sourceCode = sourceCode,
androidProxyServer,
).getHeaders();
);
try {
headers = service.getHeaders();
} finally {
service.dispose();
}
}
final updatedSource = Source()

View file

@ -1,12 +1,11 @@
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/models/source.dart';
List<dynamic> getFilterList({
required Source source,
String androidProxyServer = "",
}) {
return getExtensionService(
source,
androidProxyServer,
).getFilterList().filters;
List<dynamic> getFilterList({required Source source}) {
final service = getExtensionService(source, "");
try {
return service.getFilterList().filters;
} finally {
service.dispose();
}
}

View file

@ -57,19 +57,19 @@ Future<(String, EpubBook?)> getHtmlContent(
chapter.manga.value!.source!,
chapter.manga.value!.sourceId,
);
String? html;
final proxyServer = ref.read(androidProxyServerStateProvider);
if (htmlContent != null) {
html = await getExtensionService(
source!,
proxyServer,
).cleanHtmlContent(htmlContent);
} else {
html = await getExtensionService(
source!,
proxyServer,
).getHtmlContent(chapter.manga.value!.name!, chapter.url!);
}
final html = await withExtensionService(source!, proxyServer, (
service,
) async {
if (htmlContent != null) {
return await service.cleanHtmlContent(htmlContent);
} else {
return await service.getHtmlContent(
chapter.manga.value!.name!,
chapter.url!,
);
}
});
result = (_buildHtml(html.substring(1, html.length - 1)), null);
}

View file

@ -7,8 +7,13 @@ part 'get_source_baseurl.g.dart';
@riverpod
String sourceBaseUrl(Ref ref, {required Source source}) {
return getExtensionService(
final service = getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).sourceBaseUrl;
);
try {
return service.sourceBaseUrl;
} finally {
service.dispose();
}
}

View file

@ -2,9 +2,11 @@ import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/source_preference.dart';
import 'package:mangayomi/models/source.dart';
List<SourcePreference> getSourcePreference({
required Source source,
String androidProxyServer = "",
}) {
return getExtensionService(source, androidProxyServer).getSourcePreferences();
List<SourcePreference> getSourcePreference({required Source source}) {
final service = getExtensionService(source, "");
try {
return service.getSourcePreferences();
} finally {
service.dispose();
}
}

View file

@ -98,6 +98,7 @@ class GetIsolateService {
isolateData.sendPort.send(receivePort.sendPort);
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
final responsePort = message['responsePort'] as SendPort;
try {
final url = message['url'] as String?;
final page = message['page'] as int?;
@ -107,53 +108,38 @@ class GetIsolateService {
final proxyServer = message['proxyServer'] as String?;
final serviceType = message['serviceType'] as String?;
final useLoggerValue = message['useLogger'] as bool?;
final responsePort = message['responsePort'] as SendPort;
cfPort = message['cfPort'] as int;
if (useLoggerValue != null) {
useLogger = useLoggerValue;
}
if (serviceType == 'getDetail') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getDetail(url!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getPopular') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getPopular(page!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getLatestUpdates') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getLatestUpdates(page!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'search') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).search(query!, page!, filterList!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getVideoList') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getVideoList(url!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getPageList') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getPageList(url!);
responsePort.send({'success': true, 'data': result});
}
final result = await withExtensionService(
source!,
proxyServer ?? '',
(service) async {
switch (serviceType) {
case 'getDetail':
return await service.getDetail(url!);
case 'getPopular':
return await service.getPopular(page!);
case 'getLatestUpdates':
return await service.getLatestUpdates(page!);
case 'search':
return await service.search(query!, page!, filterList!);
case 'getVideoList':
return await service.getVideoList(url!);
case 'getPageList':
return await service.getPageList(url!);
default:
throw Exception('Unknown service type: $serviceType');
}
},
);
responsePort.send({'success': true, 'data': result});
} catch (e) {
final responsePort = message['responsePort'] as SendPort;
responsePort.send({'success': false, 'error': e.toString()});
} finally {
useLogger = false;
}
useLogger = false;
} else if (message == 'dispose') {
receivePort.close();
}

View file

@ -6,8 +6,11 @@ part 'supports_latest.g.dart';
@riverpod
bool supportsLatest(Ref ref, {required Source source}) {
return getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).supportsLatest;
final androidProxy = ref.read(androidProxyServerStateProvider);
final service = getExtensionService(source, androidProxy);
try {
return service.supportsLatest;
} finally {
service.dispose();
}
}

View file

@ -25,10 +25,12 @@ Map<String, String> headers(
if (fromSource != null && fromSource.isNotEmpty) {
headers.addAll((jsonDecode(fromSource) as Map).toMapStringString!);
}
headers.addAll(
getExtensionService(mSource, androidProxyServer).getHeaders(),
);
final service = getExtensionService(mSource, androidProxyServer);
try {
headers.addAll(service.getHeaders());
} finally {
service.dispose();
}
headers.addAll(MClient.getCookiesPref(mSource.baseUrl!));
}