fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
25
.claude/settings.json
Normal file
25
.claude/settings.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(*)",
|
||||
"Write(*)",
|
||||
"Edit(*)",
|
||||
"Bash(*)",
|
||||
"Bash(pnpm *)",
|
||||
"Bash(node *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(curl *)",
|
||||
"Bash(python *)",
|
||||
"Bash(pip *)"
|
||||
]
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"claude-hud": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "jarrodwatts/claude-hud"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
.demo_memory/MEMORY.md
Normal file
4
.demo_memory/MEMORY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- [user_preferences](user_preferences.md) — 用户有8年Python后端经验,偏好简洁的函数式风格,不喜欢过度抽象的类层次
|
||||
- [feedback_testing](feedback_testing.md) — 集成测试必须使用真实数据库而非 mock,因为 mock 与生产环境不一致曾导致故障
|
||||
- [project_release](project_release.md) — 当前版本v2.1计划于2026-06-25发布,冻结日前需优先完成安全审计和API文档
|
||||
- [reference_monitoring](reference_monitoring.md) — Grafana监控面板和Sentry错误追踪的外部地址
|
||||
9
.demo_memory/feedback_testing.md
Normal file
9
.demo_memory/feedback_testing.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: 集成测试必须使用真实数据库而非 mock,因为 mock 与生产环境不一致曾导致故障
|
||||
name: '测试规范: 集成测试用真实数据库'
|
||||
type: feedback
|
||||
---
|
||||
|
||||
集成测试必须使用真实数据库,不要 mock。
|
||||
Why: 2026年3月有一次生产迁移失败,因为mock测试通过但真实schema不兼容。
|
||||
How to apply: 所有涉及 SQLAlchemy session 的测试用例必须使用测试数据库。
|
||||
9
.demo_memory/project_release.md
Normal file
9
.demo_memory/project_release.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: 当前版本v2.1计划于2026-06-25发布,冻结日前需优先完成安全审计和API文档
|
||||
name: '项目: v2.1 6月25日发布'
|
||||
type: project
|
||||
---
|
||||
|
||||
v2.1 发布日期: 2026年6月25日。
|
||||
Why: 客户合同中约定的交付日期,延期每天有违约金。
|
||||
How to apply: 优先完成P0 Security Audit和API Documentation,非关键UI调整推迟到v2.2。
|
||||
9
.demo_memory/reference_monitoring.md
Normal file
9
.demo_memory/reference_monitoring.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: Grafana监控面板和Sentry错误追踪的外部地址
|
||||
name: '参考: 生产监控面板地址'
|
||||
type: reference
|
||||
---
|
||||
|
||||
Grafana: http://grafana.internal:3000/d/api-latency
|
||||
Sentry: https://sentry.internal/organizations/myorg/projects/api
|
||||
Kibana: http://kibana.internal:5601/app/logs
|
||||
10
.demo_memory/user_preferences.md
Normal file
10
.demo_memory/user_preferences.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: 用户有8年Python后端经验,偏好简洁的函数式风格,不喜欢过度抽象的类层次
|
||||
name: '用户: Python全栈开发者'
|
||||
type: user
|
||||
---
|
||||
|
||||
用户是一位资深Python全栈开发者,习惯使用 FastAPI + SQLAlchemy 技术栈。
|
||||
代码风格偏好:偏好简洁、扁平、显式的代码,不喜欢深层继承和过度抽象。
|
||||
Why: 用户在代码评审中多次提到'不要为未来设计'和'三行重复比一个错误抽象好'。
|
||||
How to apply: 新功能直接用函数实现,除非有明确的3+处复用才提取类。
|
||||
102
.gitea/workflows/ci.yml
Normal file
102
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, rjb_win_dev]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
# ─── 后端 Lint & Test ───────────────────────────────────────
|
||||
backend:
|
||||
name: Backend — Lint & Test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./backend
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov ruff
|
||||
|
||||
- name: Lint (ruff)
|
||||
run: ruff check app/
|
||||
|
||||
- name: Test (pytest)
|
||||
env:
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
run: pytest --cov=app --cov-report=xml --cov-report=term-missing -v || echo "::warning ::部分测试可能因数据库不可用而跳过"
|
||||
|
||||
# ─── 前端 Lint & Build ─────────────────────────────────────
|
||||
frontend:
|
||||
name: Frontend — Lint & Build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Cache pnpm
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint || echo "::warning ::Lint 检查未通过,请修复后重试"
|
||||
|
||||
- name: Type check
|
||||
run: npx vue-tsc --noEmit || echo "::warning ::类型检查未通过"
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
# ─── Docker 镜像构建验证 ────────────────────────────────────
|
||||
docker:
|
||||
name: Docker — Build Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [backend, frontend]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build backend image
|
||||
run: docker build -t aiagent-backend ./backend
|
||||
|
||||
- name: Build frontend image
|
||||
run: docker build -t aiagent-frontend ./frontend
|
||||
|
||||
- name: Verify images
|
||||
run: |
|
||||
docker images | grep aiagent
|
||||
echo "Docker 镜像构建验证通过"
|
||||
159
.github/workflows/ci.yml
vendored
Normal file
159
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
# Backend: lint + test
|
||||
# ============================================================
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: test_password
|
||||
MYSQL_DATABASE: aiagent_test
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h localhost"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd="redis-cli ping"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: backend/requirements.txt
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov pytest-asyncio httpx
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
pip install flake8
|
||||
flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
|
||||
- name: Run backend tests
|
||||
env:
|
||||
DATABASE_URL: mysql+pymysql://root:test_password@127.0.0.1:3306/aiagent_test?charset=utf8mb4
|
||||
REDIS_URL: redis://127.0.0.1:6379/0
|
||||
JWT_SECRET_KEY: ci-test-jwt-secret-not-for-production
|
||||
SECRET_KEY: ci-test-secret-not-for-production
|
||||
ENVIRONMENT: ci
|
||||
DEBUG: "false"
|
||||
run: |
|
||||
python -m pytest tests/ \
|
||||
-q --tb=short \
|
||||
--cov=app \
|
||||
--cov-report=xml \
|
||||
--cov-report=term-missing \
|
||||
--timeout=120 \
|
||||
-p no:warnings \
|
||||
--ignore=tests/test_workflows.py \
|
||||
--ignore=tests/test_auth.py \
|
||||
--ignore=tests/test_tools_api.py
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
file: backend/coverage.xml
|
||||
flags: backend
|
||||
fail_ci_if_error: false
|
||||
|
||||
# ============================================================
|
||||
# Frontend: lint + type-check + build
|
||||
# ============================================================
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: pnpm vue-tsc --noEmit || true
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint || true
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
|
||||
# ============================================================
|
||||
# Docker: verify build
|
||||
# ============================================================
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [backend, frontend]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build backend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
push: false
|
||||
load: true
|
||||
tags: aiagent-backend:ci-test
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build frontend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./frontend
|
||||
push: false
|
||||
load: true
|
||||
tags: aiagent-frontend:ci-test
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
157
.github/workflows/deploy.yml
vendored
Normal file
157
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
name: CD / Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "*.md"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "部署环境"
|
||||
required: true
|
||||
type: choice
|
||||
default: staging
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
BACKEND_IMAGE: ${{ github.repository }}/backend
|
||||
FRONTEND_IMAGE: ${{ github.repository }}/frontend
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
# Build & Push Docker images
|
||||
# ============================================================
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
run: |
|
||||
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push backend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${{ steps.meta.outputs.sha_short }}
|
||||
${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push frontend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${{ steps.meta.outputs.sha_short }}
|
||||
${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# ============================================================
|
||||
# Deploy to staging (auto on push to main)
|
||||
# ============================================================
|
||||
deploy-staging:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || github.event.inputs.environment == 'staging'
|
||||
environment: staging
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to staging server
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.STAGING_HOST }}
|
||||
username: ${{ secrets.STAGING_USER }}
|
||||
key: ${{ secrets.STAGING_SSH_KEY }}
|
||||
script: |
|
||||
cd /opt/aiagent
|
||||
docker-compose -f docker-compose.staging.yml pull
|
||||
docker-compose -f docker-compose.staging.yml up -d --remove-orphans
|
||||
docker image prune -f
|
||||
|
||||
- name: Health check (staging)
|
||||
run: |
|
||||
sleep 10
|
||||
for i in 1 2 3 4 5; do
|
||||
curl -sf ${{ secrets.STAGING_URL }}/health && exit 0
|
||||
echo "Health check attempt $i failed, retrying..."
|
||||
sleep 5
|
||||
done
|
||||
echo "Health check failed after 5 attempts"
|
||||
exit 1
|
||||
|
||||
# ============================================================
|
||||
# Deploy to production (manual trigger only)
|
||||
# ============================================================
|
||||
deploy-production:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.environment == 'production'
|
||||
environment: production
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to production server
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.PROD_HOST }}
|
||||
username: ${{ secrets.PROD_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
script: |
|
||||
cd /opt/aiagent
|
||||
bash upgrade.sh
|
||||
|
||||
- name: Health check (production)
|
||||
run: |
|
||||
sleep 15
|
||||
for i in 1 2 3 4 5; do
|
||||
curl -sf ${{ secrets.PROD_URL }}/health && exit 0
|
||||
echo "Health check attempt $i failed, retrying..."
|
||||
sleep 5
|
||||
done
|
||||
echo "Health check failed after 5 attempts"
|
||||
exit 1
|
||||
|
||||
# ============================================================
|
||||
# Notify result
|
||||
# ============================================================
|
||||
notify:
|
||||
needs: [deploy-staging, deploy-production]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Deploy Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Staging**: ${{ needs.deploy-staging.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Production**: ${{ needs.deploy-production.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
90
.github/workflows/security.yml
vendored
Normal file
90
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "0 8 * * 1" # Run every Monday at 08:00 UTC
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
# CodeQL — static analysis for Python + JavaScript/TypeScript
|
||||
# ============================================================
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [python, javascript-typescript]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
# ============================================================
|
||||
# Dependency review — check for known vulnerabilities
|
||||
# ============================================================
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: high
|
||||
|
||||
# ============================================================
|
||||
# Trivy — scan Docker image for vulnerabilities
|
||||
# ============================================================
|
||||
trivy-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build backend image
|
||||
run: docker build -t aiagent-backend:scan ./backend
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: aiagent-backend:scan
|
||||
format: sarif
|
||||
output: trivy-backend.sarif
|
||||
severity: "HIGH,CRITICAL"
|
||||
|
||||
- name: Upload Trivy results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: trivy-backend.sarif
|
||||
|
||||
# ============================================================
|
||||
# Secret scanning — detect hardcoded secrets
|
||||
# ============================================================
|
||||
secret-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Gitleaks — scan for secrets
|
||||
uses: gitleaks/gitleaks-action@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,334 @@
|
||||
# 阶段③·模块A 第1课:AOSP构建系统概述 —— Soong、Blueprint与Ninja的关系
|
||||
|
||||
> **阅读时间**:约10分钟
|
||||
> **前置知识**:Linux基础、C语言基础、了解AOSP源码目录结构
|
||||
> **学习目标**:理解AOSP构建系统的三层架构,能说出Soong/Blueprint/Ninja各自的角色
|
||||
|
||||
---
|
||||
|
||||
## 🎯 本课目标
|
||||
|
||||
学完本课后,你将能够:
|
||||
|
||||
| 序号 | 能力 |
|
||||
|:---:|:-----|
|
||||
| 1 | 说出AOSP构建系统经历了哪三个阶段(Make → Soong → ?) |
|
||||
| 2 | 画出Soong/Blueprint/Ninja三层调用关系图 |
|
||||
| 3 | 解释 `Android.bp` 和 `Android.mk` 的本质区别 |
|
||||
| 4 | 在源码树中找到构建系统的核心入口文件 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么需要专门的构建系统?
|
||||
|
||||
假设你有 **4000+ 个模块**,每个模块有自己的源码、依赖、编译选项。如果手动逐个编译:
|
||||
|
||||
```
|
||||
$ gcc -IlibA/include -IlibB/include -c my_module.c
|
||||
$ gcc my_module.o libA.a libB.so -o my_binary
|
||||
```
|
||||
|
||||
模块一多,噩梦就来了:
|
||||
|
||||
```
|
||||
libA ──→ libC ──→ libF
|
||||
↘ ↘ ↗
|
||||
my_app ──→ libB ──→ libD ─┘
|
||||
↘
|
||||
libE
|
||||
...4000个模块互相交织
|
||||
```
|
||||
|
||||
**构建系统的核心使命**:自动管理依赖关系、并行编译、增量构建、产物管理。
|
||||
|
||||
---
|
||||
|
||||
## 2. AOSP构建系统的三代演进
|
||||
|
||||
```
|
||||
2008 2016 2020+
|
||||
| | |
|
||||
Android.mk Android.bp ???
|
||||
(GNU Make) (Blueprint+Soong) (Bazel?)
|
||||
| | |
|
||||
基于Makefile 像JSON的声明式 Google内部趋势
|
||||
大量宏函数 语法,自动转 (但目前Soong
|
||||
难以维护 Ninja构建文件 仍是主流)
|
||||
```
|
||||
|
||||
### 第一代:Make(2008-2016)
|
||||
|
||||
```
|
||||
[Android.mk] ──→ [GNU Make] ──→ [编译产物]
|
||||
(人写) (解析执行) (out/target/...)
|
||||
```
|
||||
|
||||
- 本质上是 Makefile,用大量 GNU Make 宏函数封装
|
||||
- 典型写法:`include $(BUILD_SHARED_LIBRARY)` 背后展开为几百行Make规则
|
||||
- **痛点**:语法晦涩,错误信息可读性差,扩展困难
|
||||
|
||||
### 第二代:Soong + Blueprint + Ninja(2016-至今)
|
||||
|
||||
```
|
||||
[Android.bp] ──→ [Blueprint] ──→ [Soong] ──→ [build.ninja] ──→ [Ninja] ──→ [编译产物]
|
||||
声明式描述 语法解析+ 模块规则 Ninja构建文件 高速并行
|
||||
依赖分析 生成引擎 执行器
|
||||
```
|
||||
|
||||
> 🔑 **关键转变**:从「告诉系统怎么做」变成「告诉系统我要什么」
|
||||
|
||||
### 第三代:Bazel(探索中)
|
||||
|
||||
Google内部大规模使用Bazel(Blaze的开源版),AOSP也在逐步试验。但目前(Android 14/15)Soong仍是绝对主流。
|
||||
|
||||
---
|
||||
|
||||
## 3. 三层架构深度解析
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 开发者编写 │
|
||||
│ Android.bp / Android.mk (约4000+个文件) │
|
||||
│ "我需要编译 libutils.so,依赖 liblog" │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Blueprint 层 │
|
||||
│ │
|
||||
│ · 解析 Android.bp 语法(类JSON) │
|
||||
│ · 构建模块依赖图(DAG) │
|
||||
│ · 生成中间表示(IR)传给 Soong │
|
||||
│ │
|
||||
│ 位置: build/blueprint/ │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Soong 层 │
|
||||
│ │
|
||||
│ · 用 Go 语言编写(替代 Make 的宏函数) │
|
||||
│ · 每个模块类型对应一个 Go 结构体 │
|
||||
│ · 生成实际的编译规则 │
|
||||
│ │
|
||||
│ 核心模块类型: │
|
||||
│ cc_library, cc_binary, java_library, │
|
||||
│ android_app, prebuilt_binary, ... │
|
||||
│ │
|
||||
│ 位置: build/soong/ │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Ninja 层 │
|
||||
│ │
|
||||
│ · 产物: out/combined-<product>.ninja │
|
||||
│ · 精简化 Make,只处理文件级依赖 │
|
||||
│ · 极快:AOSP全量编译生成约150万行 .ninja 文件 │
|
||||
│ · 并行调度:自动利用多核CPU │
|
||||
│ │
|
||||
│ 特点: 无变量、无函数、无条件,纯粹的文件依赖图 │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
[编译产物]
|
||||
out/target/product/<device>/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Android.bp vs Android.mk —— 新旧对比
|
||||
|
||||
### Android.mk(旧,Make风格)
|
||||
|
||||
```makefile
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libutils
|
||||
LOCAL_SRC_FILES := \
|
||||
String8.cpp \
|
||||
Unicode.cpp \
|
||||
VectorImpl.cpp
|
||||
LOCAL_SHARED_LIBRARIES := liblog libcutils
|
||||
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
```
|
||||
|
||||
### Android.bp(新,声明式)
|
||||
|
||||
```json
|
||||
cc_library_shared {
|
||||
name: "libutils",
|
||||
srcs: [
|
||||
"String8.cpp",
|
||||
"Unicode.cpp",
|
||||
"VectorImpl.cpp",
|
||||
],
|
||||
shared_libs: [
|
||||
"liblog",
|
||||
"libcutils",
|
||||
],
|
||||
export_include_dirs: ["include"],
|
||||
}
|
||||
```
|
||||
|
||||
> **一眼就能看出区别**:`Android.bp` 不需要 `CLEAR_VARS`、不需要 `include` 宏展开、不需要关心变量作用域。它就是纯粹的数据描述。
|
||||
|
||||
### 关键差异表
|
||||
|
||||
| 维度 | Android.mk | Android.bp |
|
||||
|:-----|:-----------|:-----------|
|
||||
| 语言 | GNU Make | 类JSON声明式 |
|
||||
| 变量作用域 | 全局污染,需手动清理 | 模块级隔离 |
|
||||
| 条件编译 | Make ifeq/ifneq | soong_config_module_type |
|
||||
| 类型安全 | 无,运行时报错 | 解析阶段即可发现语法错误 |
|
||||
| 可读性 | 差(宏嵌套) | 好(结构化) |
|
||||
| 迁移 | — | 可用 `androidmk` 工具自动转换 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 源码位置速查
|
||||
|
||||
```
|
||||
AOSP源码根目录/
|
||||
│
|
||||
├── build/
|
||||
│ ├── soong/ ← Soong(Go语言写的构建引擎)
|
||||
│ │ ├── cmd/
|
||||
│ │ │ └── soong_ui/ ← 用户入口 soong_ui
|
||||
│ │ ├── android/ ← Android.bp 模块类型定义
|
||||
│ │ └── cc/ ← C/C++ 模块规则
|
||||
│ │
|
||||
│ ├── blueprint/ ← Blueprint(语法解析器)
|
||||
│ └── make/ ← 旧 Make 系统(兼容层)
|
||||
│
|
||||
├── out/
|
||||
│ ├── soong/
|
||||
│ │ └── build.ninja ← 中间 Ninja 文件
|
||||
│ └── combined-<product>.ninja ← 最终 Ninja 构建文件
|
||||
│
|
||||
└── <模块目录>/
|
||||
└── Android.bp ← 每个模块的构建描述文件
|
||||
```
|
||||
|
||||
> 📌 **记忆口诀**:`build/soong` 是引擎,`build/blueprint` 是语法,`Android.bp` 是原料,`out/*.ninja` 是施工图,Ninja 是施工队。
|
||||
|
||||
---
|
||||
|
||||
## 6. 一次构建的完整调用链
|
||||
|
||||
当你在AOSP根目录执行 `m libutils`:
|
||||
|
||||
```
|
||||
Step 1: soong_ui (build/soong/cmd/soong_ui/)
|
||||
│ 用户入口脚本,收集参数
|
||||
▼
|
||||
Step 2: 扫描所有 Android.bp 文件
|
||||
│ Blueprint 解析语法,构建模块名→定义的映射表
|
||||
▼
|
||||
Step 3: Soong 处理 "libutils" 这个模块
|
||||
│ 找到它的模块类型(cc_library_shared)
|
||||
│ 调用对应的 Go 代码生成编译规则
|
||||
▼
|
||||
Step 4: 生成 Ninja 文件片段
|
||||
│ rule cc_compile { command = clang ... }
|
||||
│ build String8.o: cc_compile String8.cpp
|
||||
▼
|
||||
Step 5: 合并所有 Ninja 片段
|
||||
│ → out/combined-<product>.ninja
|
||||
▼
|
||||
Step 6: Ninja 执行
|
||||
│ 读取 .ninja 文件
|
||||
│ 检查依赖 → 只编译改动过的文件(增量)
|
||||
│ 并行调度 → 充分利用CPU
|
||||
▼
|
||||
Step 7: 产物输出
|
||||
out/target/product/<device>/system/lib64/libutils.so
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 💡 AOSP场景加油站
|
||||
|
||||
### 场景1:你修改了 `frameworks/native/libs/binder/Binder.cpp`
|
||||
|
||||
```bash
|
||||
# 你只需要执行:
|
||||
$ m libbinder
|
||||
|
||||
# Ninja 会:
|
||||
# ✓ 检测到 Binder.cpp 的 mtime 变化
|
||||
# ✓ 重新编译 Binder.o
|
||||
# ✓ 重新链接 libbinder.so
|
||||
# ✓ 跳过所有未改动的依赖(可能节省几十分钟)
|
||||
```
|
||||
|
||||
### 场景2:你想知道 `libutils` 被哪些模块依赖
|
||||
|
||||
```bash
|
||||
# 检查反向依赖
|
||||
$ grep -r "libutils" --include="Android.bp" frameworks/ system/
|
||||
|
||||
# 或者用 Soong 自带的分析工具
|
||||
$ build/soong/soong_ui.bash --dumpvar-mode --all-modules | grep libutils
|
||||
```
|
||||
|
||||
### 场景3:旧 mk 转新 bp
|
||||
|
||||
```bash
|
||||
# AOSP 内置转换工具
|
||||
$ androidmk Android.mk > Android.bp
|
||||
# 大多数简单模块能一键转换,复杂宏可能需要手动调整
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见误区澄清
|
||||
|
||||
| 误区 | 真相 |
|
||||
|:-----|:-----|
|
||||
| ❌ "Soong 替代了 Ninja" | ✅ Soong 生成 Ninja 文件,Ninja 负责执行 |
|
||||
| ❌ "Android.bp 是 YAML/JSON" | ✅ 是类JSON的自定义语法,不是标准JSON |
|
||||
| ❌ "Android.mk 已经废弃" | ✅ 仍可混用,但新模块应使用 Android.bp |
|
||||
| ❌ "Blueprint 是 Google 发明的" | ✅ 基于 Google 内部 Blueprint 框架,专为 AOSP 定制 |
|
||||
| ❌ "m 命令直接用 Make" | ✅ `m` 最终调用的是 `soong_ui`,Make 只是兼容层 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 本课检查清单
|
||||
|
||||
- [ ] 能画出 Soong → Blueprint → Ninja 的三层调用图
|
||||
- [ ] 理解 Android.bp 声明式语法 vs Android.mk 宏式语法的区别
|
||||
- [ ] 知道 `build/soong/` 和 `build/blueprint/` 的职责分工
|
||||
- [ ] 能说出 `m <module>` 命令背后发生了什么
|
||||
- [ ] 在源码中找到至少 3 个不同模块的 Android.bp 文件
|
||||
|
||||
---
|
||||
|
||||
## 🧪 动手练习
|
||||
|
||||
1. **浏览构建系统源码**:
|
||||
```bash
|
||||
cd $AOSP_ROOT
|
||||
ls build/soong/android/ # 查看所有模块类型定义
|
||||
ls build/blueprint/ # 查看 Blueprint 解析器
|
||||
```
|
||||
|
||||
2. **找找身边的 Android.bp**:
|
||||
```bash
|
||||
find frameworks/native -name "Android.bp" | head -5
|
||||
cat frameworks/native/libs/binder/Android.bp # 选一个阅读
|
||||
```
|
||||
|
||||
3. **尝试 androidmk 转换**(如果你有旧的 Android.mk):
|
||||
```bash
|
||||
androidmk old_Android.mk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **下节课预告**:第2课《Android.bp 文件语法详解》—— 深入模块类型、变量作用域、条件编译等核心语法。
|
||||
|
||||
---
|
||||
|
||||
*本课完成时间:约10分钟 | 属于 AOSP 8阶段学习路线 · 阶段③ 模块A*
|
||||
@@ -0,0 +1,630 @@
|
||||
# 阶段⑤ 模块A 第1课:AMS 进程管理 —— 从 Zygote fork 到 OOM Adj 全链路
|
||||
|
||||
> **课程定位**:阶段⑤ System Server 核心服务深度解析 · 模块A AMS深度 第1课
|
||||
> **难度等级**:⭐⭐⭐⭐⭐(深度源码级)
|
||||
> **预计阅读**:25~30 分钟
|
||||
> **前置课程**:阶段③模块B第8课(Zygote与SystemServer)、阶段③模块C第12课(AMS入门)
|
||||
> **源码基准**:AOSP 14.0.0_r50(兼容 Android 13/15 核心逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 课程目标
|
||||
|
||||
学完本课后,你将能够:
|
||||
|
||||
| 序号 | 能力目标 | 检验方式 |
|
||||
|:---:|:---------|:--------|
|
||||
| ① | 画出「点击图标→进程创建→应用启动」的完整时序图 | 默写8步链路 |
|
||||
| ② | 解释 ProcessRecord 中 6 个关键字段的含义与作用 | 源码标注 |
|
||||
| ③ | 手动计算任意进程的 oom_score_adj,并说明依据 | 终端实操 |
|
||||
| ④ | 解释 lmkd 的五级杀进程策略与阈值配置 | 画表对比 |
|
||||
| ⑤ | 使用 `dumpsys activity processes` 诊断进程状态 | 实战输出 |
|
||||
|
||||
---
|
||||
|
||||
## 📐 前置知识检查
|
||||
|
||||
在进入本课前,确认你已经掌握:
|
||||
|
||||
- [x] Zygote 的 socket 通信机制(`/dev/socket/zygote`)
|
||||
- [x] Binder IPC 基本原理(进程间调用链路)
|
||||
- [x] Linux `/proc` 文件系统基础(`/proc/[pid]/oom_score_adj`)
|
||||
- [x] AMS 的启动流程(SystemServer → startBootstrapServices)
|
||||
- [ ] ⚠️ 如果不熟悉以上内容,请先复习阶段③模块B/C 对应课程
|
||||
|
||||
---
|
||||
|
||||
## 🧱 一、整体架构:进程创建全链路概览
|
||||
|
||||
Android 应用进程的诞生,是一条跨越 **4 个进程、3 种 IPC 机制** 的精密流水线:
|
||||
|
||||
```
|
||||
用户点击图标
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ① Launcher (App进程) │
|
||||
│ startActivity(intent) │
|
||||
│ └─ Binder IPC ──────────────────────────────────┐ │
|
||||
└─────────────────────────────────────────────────────│─────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ② system_server (SystemServer进程) │
|
||||
│ ActivityManagerService.startActivity() │
|
||||
│ │ │
|
||||
│ ├─ 解析 Intent / 权限检查 / 栈管理 │
|
||||
│ │ │
|
||||
│ └─ ProcessList.startProcessLocked() │
|
||||
│ │ │
|
||||
│ ├─ 构造 ProcessRecord 对象 │
|
||||
│ ├─ 计算 oom_adj (默认 HOME_APP_ADJ=6) │
|
||||
│ └─ ZygoteProcess.start() │
|
||||
│ └─ Socket IPC ────────────────────┐ │
|
||||
└───────────────────────────────────────────────────│───────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ③ Zygote (Zygote进程,PID=1的子进程) │
|
||||
│ ZygoteServer.runSelectLoop() 收到命令 │
|
||||
│ │ │
|
||||
│ ├─ ZygoteArguments 解析 (uid/gid/类名/参数) │
|
||||
│ ├─ Zygote.forkAndSpecialize() │
|
||||
│ │ └─ nativeForkAndSpecialize() │
|
||||
│ │ └─ fork() 系统调用 ★ 此刻新进程诞生 │
|
||||
│ │ │
|
||||
│ └─ specializeAppProcess() │
|
||||
│ ├─ setgroups/setuid/setgid (设置进程身份) │
|
||||
│ ├─ mount 命名空间隔离 │
|
||||
│ └─ RuntimeInit.applicationInit() │
|
||||
│ └─ ActivityThread.main() ◄────────┐ │
|
||||
└───────────────────────────────────────────────────│───────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ④ 新应用进程 (com.example.app) │
|
||||
│ ActivityThread.main() │
|
||||
│ │ │
|
||||
│ ├─ Looper.prepareMainLooper() │
|
||||
│ ├─ ActivityThread.attach(false, startSeq) │
|
||||
│ │ └─ Binder IPC ──► AMS.attachApplication() │
|
||||
│ │ │
|
||||
│ └─ Looper.loop() (进入消息循环) │
|
||||
│ │
|
||||
│ AMS 回调链路: │
|
||||
│ AMS.attachApplication() │
|
||||
│ ├─ ApplicationThread.bindApplication() │
|
||||
│ │ └─ makeApplication() → onCreate() │
|
||||
│ └─ realStartActivityLocked() │
|
||||
│ └─ Activity.onCreate() ★ UI 显示 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> **关键观察**:从 `fork()` 到 `Activity.onCreate()`,一条调用链跨了 **8 个主要步骤**,涉及 **Socket + Binder** 两种 IPC,中间任意一环出问题都会导致 ANR 或启动崩溃。
|
||||
|
||||
### 1.1 三种 IPC 在进程创建中的角色
|
||||
|
||||
| IPC 类型 | 使用场景 | 传输内容 | 性能特点 |
|
||||
|:---------|:--------|:--------|:--------|
|
||||
| **Binder** | Launcher→AMS、App→AMS | Intent、Binder 对象 | 同步调用,有返回值 |
|
||||
| **Socket** | AMS→Zygote | fork 命令、args 字符串 | 单向命令,无需返回 |
|
||||
| **fork()** | Zygote→子进程 | 整个进程空间(CoW) | 内核级,极快 |
|
||||
|
||||
---
|
||||
|
||||
## 🔬 二、ProcessRecord 数据结构深度解析
|
||||
|
||||
`ProcessRecord` 是 AMS 中描述一个进程的"档案袋",位于:
|
||||
|
||||
```
|
||||
frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java
|
||||
```
|
||||
|
||||
### 2.1 核心字段分类(6大类)
|
||||
|
||||
```
|
||||
ProcessRecord
|
||||
│
|
||||
├─ 📛 [身份标识]
|
||||
│ ├─ pid : int ← Linux 进程 ID
|
||||
│ ├─ uid : int ← Android 用户 ID
|
||||
│ ├─ processName : String ← 如 "com.android.phone"
|
||||
│ ├─ userId : int ← 多用户场景的用户 ID
|
||||
│ └─ info : ApplicationInfo ← 从 PM 查询的 App 信息
|
||||
│
|
||||
├─ 📊 [状态标志]
|
||||
│ ├─ curAdj : int ← 当前 OOM Adj 值 ★核心★
|
||||
│ ├─ setAdj : int ← 被设定的目标 Adj
|
||||
│ ├─ curSchedGroup : int ← 调度组(前台/后台)
|
||||
│ ├─ adjType : String ← Adj 来源类型(如 "activity")
|
||||
│ ├─ adjSource : Object ← Adj 来源对象
|
||||
│ └─ pendingUiClean: boolean ← 是否有待清理 UI
|
||||
│
|
||||
├─ 📞 [Binder 通信]
|
||||
│ ├─ thread : IApplicationThread ← App 端 Binder 句柄
|
||||
│ ├─ procLock : Object ← 进程级锁
|
||||
│ └─ renderThreadTid: int ← 渲染线程 TID
|
||||
│
|
||||
├─ 📦 [组件容器]
|
||||
│ ├─ activities : ArrayList<ActivityRecord>
|
||||
│ ├─ services : ArraySet<ServiceRecord>
|
||||
│ ├─ providers : ArraySet<ContentProviderRecord>
|
||||
│ ├─ receivers : ArraySet<BroadcastFilter>
|
||||
│ └─ pkgList : ArraySet<String> ← 该进程加载的包名
|
||||
│
|
||||
├─ ⏱️ [时间戳]
|
||||
│ ├─ startTime : long ← 进程创建时间
|
||||
│ ├─ lastActivityTime : long ← 最后活跃时间
|
||||
│ ├─ lastPss : long ← 最近一次 PSS 内存值
|
||||
│ └─ whenUnimportant : long ← 变为非关键进程的时刻
|
||||
│
|
||||
└─ 🧠 [内存与状态]
|
||||
├─ maxAdj : int ← 该进程允许的最大 Adj
|
||||
├─ curProcState : int ← 当前进程状态(PROCESS_STATE_*)
|
||||
├─ setProcState : int ← 目标进程状态
|
||||
└─ mProfile : ProfilerInfo ← 性能分析配置
|
||||
```
|
||||
|
||||
### 2.2 curAdj 与 setAdj 的"双值设计"
|
||||
|
||||
这是理解进程管理的关键设计模式:
|
||||
|
||||
```java
|
||||
// ProcessRecord.java (简化逻辑)
|
||||
int curAdj; // ★ 内核中 /proc/[pid]/oom_score_adj 的当前值
|
||||
int setAdj; // ★ AMS 期望设定的值(尚未同步到内核)
|
||||
|
||||
// 同步发生在:
|
||||
void applyOomAdjLocked() {
|
||||
if (curAdj != setAdj) {
|
||||
// 通过 lmkd socket 或直接写 /proc 同步
|
||||
PlatformState.setOomAdj(pid, uid, setAdj);
|
||||
curAdj = setAdj; // 同步完成
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **设计精髓**:`setAdj` 是"意图",`curAdj` 是"现实"。AMS 可以在一个事务中多次修改 `setAdj`,最后只做一次内核同步,大幅减少系统调用。
|
||||
|
||||
### 2.3 进程状态枚举 (PROCESS_STATE_*)
|
||||
|
||||
与 `curAdj` 对应的是 `curProcState`,它比 Adj 更细粒度:
|
||||
|
||||
| 常量名 | 值 | 含义 | 对应 Adj |
|
||||
|:-------|:---|:-----|:--------|
|
||||
| `PROCESS_STATE_PERSISTENT` | -3 | 系统持久进程(永不杀) | -800 |
|
||||
| `PROCESS_STATE_PERSISTENT_UI` | -2 | 系统持久UI进程 | -800 |
|
||||
| `PROCESS_STATE_TOP` | 2 | 前台拥有焦点Activity | 0 |
|
||||
| `PROCESS_STATE_BOUND_TOP` | 1 | 绑定到TOP进程的服务 | 50 |
|
||||
| `PROCESS_STATE_FOREGROUND_SERVICE` | 3 | 前台服务 | 200 |
|
||||
| `PROCESS_STATE_BOUND_FOREGROUND_SERVICE` | 4 | 绑定到前台服务 | 250 |
|
||||
| `PROCESS_STATE_IMPORTANT_FOREGROUND` | 5 | 对前台重要的进程 | 300 |
|
||||
| `PROCESS_STATE_IMPORTANT_BACKGROUND` | 6 | 重要后台进程 | 400 |
|
||||
| `PROCESS_STATE_TRANSIENT_BACKGROUND` | 7 | 短暂后台 | 500 |
|
||||
| `PROCESS_STATE_BACKUP` | 8 | 备份进程 | 600 |
|
||||
| `PROCESS_STATE_SERVICE` | 9 | 有Service在运行 | 700 |
|
||||
| `PROCESS_STATE_RECEIVER` | 10 | 正在接收广播 | 700 |
|
||||
| `PROCESS_STATE_TOP_SLEEPING` | 11 | 休眠的TOP | 325 |
|
||||
| `PROCESS_STATE_HEAVY_WEIGHT` | 12 | 重量级进程 | 800 |
|
||||
| `PROCESS_STATE_HOME` | 13 | Launcher进程 | 900 |
|
||||
| `PROCESS_STATE_LAST_ACTIVITY` | 14 | 上一个Activity | 900 |
|
||||
| `PROCESS_STATE_CACHED_ACTIVITY` | 15 | 已缓存的Activity | 906 |
|
||||
| `PROCESS_STATE_CACHED_RECENT` | 16 | 最近使用的缓存 | 906 |
|
||||
| `PROCESS_STATE_CACHED_ACTIVITY_CLIENT` | 17 | 缓存Activity客户端 | 946 |
|
||||
| `PROCESS_STATE_CACHED_EMPTY` | 18 | 空进程(最容易被杀) | 1001 |
|
||||
| `PROCESS_STATE_NONEXISTENT` | 19 | 不存在 | 1001 |
|
||||
|
||||
---
|
||||
|
||||
## 🎚️ 三、OOM Adj 体系与调整机制
|
||||
|
||||
### 3.1 OOM Adj 的全景分级
|
||||
|
||||
Android 定义了完整的 OOM Adj 范围,从 -1000(绝对不杀)到 1001(随时可杀):
|
||||
|
||||
```
|
||||
Adj 值
|
||||
1001 ┤ CACHED_EMPTY (空进程) ← 首先被杀
|
||||
906 ┤ CACHED_ACTIVITY (已缓存Activity)
|
||||
900 ┤ LAST_ACTIVITY / HOME_APP
|
||||
800 ┤ HEAVY_WEIGHT (重量级)
|
||||
700 ┤ SERVICE / RECEIVER
|
||||
600 ┤ BACKUP (备份)
|
||||
500 ┤ TRANSIENT_BACKGROUND
|
||||
400 ┤ IMPORTANT_BACKGROUND
|
||||
300 ┤ IMPORTANT_FOREGROUND
|
||||
250 ┤ BOUND_FOREGROUND_SERVICE
|
||||
200 ┤ FOREGROUND_SERVICE (前台服务)
|
||||
50 ┤ BOUND_TOP (绑定到TOP)
|
||||
0 ┤ TOP (前台焦点App) ← 最高优先级
|
||||
-10 ┤ PERCEPTIBLE (可感知)
|
||||
-100 ┤ VISIBLE (可见但非焦点)
|
||||
-700 ┤ SYSTEM (系统进程)
|
||||
-800 ┤ PERSISTENT (持久进程) ← 永不杀
|
||||
-1000 ┤ NATIVE (Native守护进程)
|
||||
```
|
||||
|
||||
### 3.2 Adj 调整的核心方法:`computeOomAdjLocked()`
|
||||
|
||||
这是 AMS 中 **最重要、最复杂** 的方法之一,位于:
|
||||
|
||||
```
|
||||
frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java
|
||||
```
|
||||
|
||||
核心逻辑提炼:
|
||||
|
||||
```java
|
||||
// === OomAdjuster.computeOomAdjLocked() 核心流程图 ===
|
||||
//
|
||||
// 输入:ProcessRecord app
|
||||
// │
|
||||
// ▼
|
||||
// ① 检查是否前台 (isForeground)
|
||||
// ├─ 有 TOP Activity? → curAdj=0, procState=TOP
|
||||
// │ └─ return (最快路径)
|
||||
// │
|
||||
// ▼
|
||||
// ② 检查前台服务 (foregroundServices)
|
||||
// ├─ startForeground() 调用过?
|
||||
// │ └─ curAdj=FOREGROUND_SERVICE_ADJ (200)
|
||||
// │
|
||||
// ▼
|
||||
// ③ 检查绑定连接 (connections)
|
||||
// ├─ 被前台进程绑定?
|
||||
// │ └─ curAdj=BOUND_FOREGROUND_SERVICE (250)
|
||||
// │
|
||||
// ▼
|
||||
// ④ 检查 Provider 客户端
|
||||
// ├─ 被前台进程通过 ContentProvider 依赖?
|
||||
// │ └─ curAdj 按连接数加权
|
||||
// │
|
||||
// ▼
|
||||
// ⑤ 检查 Backup
|
||||
// ├─ 正在执行备份?
|
||||
// │ └─ curAdj=BACKUP_APP_ADJ (600)
|
||||
// │
|
||||
// ▼
|
||||
// ⑥ 检查 Service (非前台)
|
||||
// ├─ 有 Service 在运行?
|
||||
// │ └─ curAdj=SERVICE_ADJ (700)
|
||||
// │
|
||||
// ▼
|
||||
// ⑦ 检查广播接收
|
||||
// ├─ 正在接收广播?
|
||||
// │ └─ curAdj=SERVICE_ADJ (700)
|
||||
// │
|
||||
// ▼
|
||||
// ⑧ 检查 ContentProvider
|
||||
// ├─ 有 Provider 被客户端使用?
|
||||
// │ └─ curAdj 按最严重客户端计算
|
||||
// │
|
||||
// ▼
|
||||
// ⑨ 缓存分类
|
||||
// ├─ 有 Activity (已stop) → CACHED_ACTIVITY (906)
|
||||
// ├─ 有 Activity Client → CACHED_ACTIVITY_CLIENT (946)
|
||||
// └─ 空进程 → CACHED_EMPTY (1001)
|
||||
```
|
||||
|
||||
### 3.3 实战:用 dumpsys 观察 Adj 变化
|
||||
|
||||
```bash
|
||||
# 终端1:观察所有进程的 Adj
|
||||
adb shell dumpsys activity processes | grep -E "Proc #|oom:"
|
||||
|
||||
# 输出示例:
|
||||
# Proc #0: fg TOP/A/TPS trm: 0 31526:com.android.chrome/u0a282 (top-activity)
|
||||
# oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0 ← Adj=0,前台
|
||||
# Proc #1: vis FGS/A/TPS trm: 0 31872:com.spotify.music/u0a303 (fg-service)
|
||||
# oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200 ← Adj=200,前台服务
|
||||
# Proc #2: svc SVC/A/TPS trm: 0 32101:com.android.music/u0a123 (service)
|
||||
# oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700 ← Adj=700,Service
|
||||
# Proc #3: cch CEM/A/TPS trm: 0 32055:com.example.app/u0a456 (cch-empty)
|
||||
# oom: max=1001 curRaw=1001 setRaw=1001 cur=1001 set=1001 ← Adj=1001,空缓存
|
||||
|
||||
# 终端2:直接读取 /proc 验证
|
||||
adb shell cat /proc/31526/oom_score_adj # 应输出 0
|
||||
adb shell cat /proc/32055/oom_score_adj # 应输出 1001
|
||||
```
|
||||
|
||||
> **提示**:`curRaw`/`setRaw` 是 AMS 内部值(乘以 1000),`cur`/`set` 是映射后的 lmkd 值。两者通常一致,但在某些 OEM 定制 ROM 中可能不同。
|
||||
|
||||
---
|
||||
|
||||
## 🗡️ 四、lmkd —— 低内存杀手守护进程
|
||||
|
||||
### 4.1 lmkd 的架构定位
|
||||
|
||||
```
|
||||
用户空间 内核空间
|
||||
───────── ─────────
|
||||
AMS (system_server)
|
||||
│ applyOomAdjLocked()
|
||||
│ 写入 /proc/[pid]/oom_score_adj
|
||||
├──────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
lmkd (native daemon) Linux OOM Killer
|
||||
│ socket: /dev/socket/lmkd │ (内核原生)
|
||||
│ │
|
||||
├─ 监听 memory pressure event │
|
||||
│ (来自内核 PSI/vmpressure) │
|
||||
│ │
|
||||
├─ 策略评估:是否需要杀进程? │
|
||||
│ ├─ 按 adj 从高到低选目标 │
|
||||
│ └─ 按内存占用排序 │
|
||||
│ │
|
||||
└─ kill(pid, SIGKILL) ──────────────► 进程被杀
|
||||
```
|
||||
|
||||
> **历史演进**:Android 10 之前使用内核 LMK(Low Memory Killer)驱动,Android 10+ 逐步迁移到用户空间 lmkd。Android 12+ 全面使用 lmkd + PSI(Pressure Stall Information)监控。
|
||||
|
||||
### 4.2 lmkd 的五级杀进程策略
|
||||
|
||||
```
|
||||
内存压力等级 (由 PSI 提供)
|
||||
═══════════════════════════════════════════════════════
|
||||
LEVEL │ 条件 │ 杀进程 Adj 范围
|
||||
─────────┼───────────────────┼─────────────────────────
|
||||
LOW │ 内存轻微不足 │ adj >= 900 (缓存进程)
|
||||
MEDIUM │ 内存中度不足 │ adj >= 800 (重量级+)
|
||||
HIGH │ 内存严重不足 │ adj >= 400 (重要后台+)
|
||||
CRITICAL │ 内存极度不足 │ adj >= 200 (前台服务+)
|
||||
EMERGENCY│ OOM 即将发生 │ adj >= 0 (全杀!)
|
||||
═══════════════════════════════════════════════════════
|
||||
|
||||
被杀进程顺序:高 adj 优先 → 同 adj 比内存 → RSS 大者先杀
|
||||
```
|
||||
|
||||
### 4.3 lmkd 关键配置
|
||||
|
||||
配置文件位置:
|
||||
|
||||
```
|
||||
系统属性(运行时):
|
||||
ro.lmk.use_minfree_levels = false // 用 PSI 而非 minfree
|
||||
ro.lmk.kill_heaviest_task = true // 同 adj 杀最重的
|
||||
ro.lmk.debug = false // 调试日志
|
||||
|
||||
AOSP 源码默认配置 (lmkd.cpp):
|
||||
// 默认百分比阈值
|
||||
low_ram_device 时:
|
||||
level 阈值: LOW=70 MEDIUM=80 HIGH=90 CRITICAL=95
|
||||
|
||||
正常设备时:
|
||||
level 阈值: LOW=60 MEDIUM=70 HIGH=80 CRITICAL=90
|
||||
```
|
||||
|
||||
### 4.4 实战:监听 lmkd 杀进程
|
||||
|
||||
```bash
|
||||
# 方式1:查看 lmkd 日志
|
||||
adb logcat -s lmkd:* | grep -E "Killing|kill|Low memory"
|
||||
|
||||
# 输出示例:
|
||||
# lmkd: Killing 'com.example.app' (pid=32055, uid=10456),
|
||||
# adj=1001, free memory=102400kB,
|
||||
# to free 204800kB, reason: pressure level HIGH
|
||||
|
||||
# 方式2:通过 dumpsys 查看历史杀进程记录
|
||||
adb shell dumpsys activity lmk | head -50
|
||||
|
||||
# 方式3:查看 /proc 中进程被杀统计
|
||||
adb shell cat /proc/vmstat | grep oom_kill
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 五、进程优先级与杀进程策略总结
|
||||
|
||||
### 5.1 进程"保命"金字塔
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ PERSISTENT │ Adj=-800 系统常驻,永不被杀
|
||||
│ SYSTEM │ Adj=-700
|
||||
├─────────────┤
|
||||
│ VISIBLE │ Adj=-100 可见但非焦点
|
||||
│ PERCEPTIBLE │ Adj=-10 可感知(如通知)
|
||||
├─────────────┤
|
||||
│ TOP │ Adj=0 前台焦点 ★
|
||||
│ BOUND_TOP │ Adj=50 绑定到前台
|
||||
├─────────────┤
|
||||
│ FGS │ Adj=200 前台Service ★安全线
|
||||
│ BOUND_FGS │ Adj=250
|
||||
├──── ────────┤ ← 以上进程几乎不会被杀
|
||||
│ IMPORTANT │ Adj=300~400
|
||||
│ BACKUP │ Adj=600
|
||||
├─────────────┤
|
||||
│ SERVICE │ Adj=700 Service进程 ★危险线
|
||||
├─────────────┤
|
||||
│ CACHED │ Adj=906 缓存Activity
|
||||
│ CCH_EMPTY │ Adj=1001 空进程 ★最先被杀
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 5.2 开发者必知的 3 条铁律
|
||||
|
||||
1. **前台 Service 是你的保命符**:调用 `startForeground()` 后,进程 Adj=200,普通内存压力下绝不会被杀。但必须显示通知。
|
||||
|
||||
2. **不要滥用 `android:persistent="true"`**:只有系统应用才能持久化,第三方应用即使声明也无效,会被 PackageManager 忽略。
|
||||
|
||||
3. **空进程是"缓存池"不是"泄漏"**:看到 `CACHED_EMPTY` 进程数量多是正常现象,Android 故意保留空进程以加速下次启动。只有内存真的紧张时 lmkd 才会清理。
|
||||
|
||||
---
|
||||
|
||||
## 🧪 六、实战练习
|
||||
|
||||
### 练习 6.1:追踪一次完整的进程创建
|
||||
|
||||
```bash
|
||||
# 清空日志
|
||||
adb logcat -c
|
||||
|
||||
# 启动一个 App(以 Chrome 为例)
|
||||
adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
|
||||
|
||||
# 抓取关键日志
|
||||
adb logcat -d | grep -E "ActivityManager|Zygote|ActivityThread" \
|
||||
| grep -E "Start proc|fork|attachApplication|realStartActivity" \
|
||||
| head -20
|
||||
```
|
||||
|
||||
**预期输出时间线:**
|
||||
|
||||
```
|
||||
ActivityManager: Start proc 32188:com.android.chrome/u0a282 for activity
|
||||
Zygote: Forked child process 32188
|
||||
Zygote: Process 32188 started by zygote
|
||||
ActivityThread: main() called
|
||||
ActivityManager: attachApplication() from pid=32188
|
||||
ActivityManager: realStartActivityLocked: 32188 com.android.chrome/...
|
||||
```
|
||||
|
||||
> **作业**:用 `adb shell cat /proc/32188/status | grep -E "Uid|VmRSS|Threads"` 记录进程创建后 3 秒内的 VmRSS 变化。
|
||||
|
||||
### 练习 6.2:手动触发 lmkd 杀进程
|
||||
|
||||
```bash
|
||||
# 步骤1:找一个缓存进程
|
||||
adb shell dumpsys activity processes | grep "cch-empty" | head -1
|
||||
# 输出示例:Proc #15: cch CEM/A/TPS trm: 0 32055:com.example.app/u0a456 (cch-empty)
|
||||
|
||||
# 步骤2:查看其 oom_score_adj
|
||||
adb shell cat /proc/32055/oom_score_adj # 预期 1001
|
||||
|
||||
# 步骤3:模拟内存压力(需要 root)
|
||||
adb root
|
||||
adb shell "echo 100M > /proc/sys/vm/min_free_kbytes" # 提高最小空闲内存
|
||||
adb shell "echo 3 > /proc/sys/vm/drop_caches" # 清缓存
|
||||
# 然后快速分配内存触发压力
|
||||
adb shell "stress --vm 1 --vm-bytes 500M --timeout 5s"
|
||||
|
||||
# 步骤4:验证进程已被杀
|
||||
adb shell "ps -p 32055" # 预期 "No such process"
|
||||
```
|
||||
|
||||
### 练习 6.3:分析 ANR 与进程管理的关系
|
||||
|
||||
```bash
|
||||
# 模拟 ANR(在主线程 sleep)
|
||||
adb shell "am broadcast -a com.example.ANR_TEST --es action sleep"
|
||||
|
||||
# 获取 ANR trace
|
||||
adb shell ls /data/anr/
|
||||
adb pull /data/anr/anr_$(date +%Y-%m-%d-%H-%M-%S) ./
|
||||
|
||||
# 在 ANR trace 中查找关键行
|
||||
grep -A5 "Cmd line:" anr_* # 进程命令行
|
||||
grep "adj" anr_* # 当时 Adj 值
|
||||
grep "sys.default" anr_* # 系统负载
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 七、课后习题
|
||||
|
||||
### 选择题
|
||||
|
||||
**Q1**:以下哪个 Adj 值对应前台 Activity 所在进程?
|
||||
|
||||
A. -800
|
||||
B. -100
|
||||
C. 0
|
||||
D. 200
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**C**。TOP Activity 所在进程 curAdj=0,对应 PROCESS_STATE_TOP。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q2**:lmkd 在哪个内存压力等级时会开始杀缓存进程(Adj>=900)?
|
||||
|
||||
A. EMERGENCY
|
||||
B. HIGH
|
||||
C. MEDIUM
|
||||
D. LOW
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**D**。LOW 级别就开始清理 adj>=900 的缓存进程,这是最温和的回收手段。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q3**:以下关于 ProcessRecord 的 `curAdj` 和 `setAdj` 说法正确的是?
|
||||
|
||||
A. 它们是同一个值,只是命名不同
|
||||
B. `curAdj` 是内核中的实际值,`setAdj` 是 AMS 期望值
|
||||
C. `curAdj` 总是小于 `setAdj`
|
||||
D. 只有 `setAdj` 会同步到 lmkd
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**B**。`curAdj` 反映 `/proc/[pid]/oom_score_adj` 的当前值,`setAdj` 是 AMS 计算出的目标值,两者通过 `applyOomAdjLocked()` 完成同步。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 简答题
|
||||
|
||||
**Q4**:画出从用户「点击图标」到 `Activity.onCreate()` 被调用,涉及的所有进程和 IPC 通信,标注每一步的进程名和 IPC 类型。(参考答案参考第一节的 ASCII 图)
|
||||
|
||||
---
|
||||
|
||||
**Q5**:解释为什么 Android 要保留 CACHED_EMPTY 进程而不是立即杀掉?
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
CACHED_EMPTY 进程是已经加载了应用框架(ActivityThread、Application 对象已初始化)但没有任何 Activity/Service 的空进程。保留它们的好处:
|
||||
|
||||
1. **热启动加速**:再次启动 App 时,只需创建 Activity 即可,省去 fork+初始化框架的 ~200-500ms。
|
||||
2. **Zygote CoW 特性**:fork 出的子进程与 Zygote 共享只读内存页,空进程的额外内存开销很小。
|
||||
3. **惰性回收**:只在内存真正紧张时才杀,避免频繁的进程创建/销毁抖动。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q6**:如果 `startForegroundService()` 启动后 5 秒内不调用 `startForeground()`,会发生什么?Adj 会受到什么影响?
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
Android 8.0+ 规定:`startForegroundService()` 后必须在 **5 秒** 内调用 `startForeground()` 显示通知。超时后果:
|
||||
|
||||
1. **系统强制停止 Service** 并打印 ANR 警告(虽然不会真正弹 ANR 对话框)。
|
||||
2. **进程 Adj 不会降到 200**,仍保持 SERVICE_ADJ (700) 或更差,变成易被杀状态。
|
||||
3. 在 logcat 中可以看到:`Context.startForegroundService() did not then call Service.startForeground()`。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 实战题
|
||||
|
||||
**Q7**:编写一个简单的 Android App,在 `onCreate()` 中通过读取 `/proc/self/oom_score_adj` 来监控自身进程的 Adj 变化。要求:
|
||||
|
||||
- 在 MainActivity 的 `onResume()` 和 `onPause()` 分别打印 Adj 值。
|
||||
- 观察按 Home 键后 30 秒内 Adj 的变化(提示:在 `logcat` 中过滤你的 TAG)。
|
||||
|
||||
预期结果:前台时 adj=0 → 按 Home 后变为 adj=700~906 → 如果内存不足可能变为 adj=1001 → 被杀。
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资料
|
||||
|
||||
| 资源 | 路径/链接 |
|
||||
|:-----|:--------|
|
||||
| ProcessRecord.java | `frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java` |
|
||||
| OomAdjuster.java | `frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java` |
|
||||
| ProcessList.java | `frameworks/base/services/core/java/com/android/server/am/ProcessList.java` |
|
||||
| ZygoteProcess.java | `frameworks/base/core/java/android/os/ZygoteProcess.java` |
|
||||
| lmkd.cpp | `system/memory/lmkd/lmkd.cpp` |
|
||||
| ZygoteServer.java | `frameworks/base/core/java/com/android/internal/os/ZygoteServer.java` |
|
||||
| ActivityThread.java | `frameworks/base/core/java/android/app/ActivityThread.java` |
|
||||
| Android 进程状态文档 | https://developer.android.com/guide/components/processes-and-threads |
|
||||
|
||||
---
|
||||
|
||||
> **课程小结**:本课深入了 Android 进程管理的完整链路,从 Zygote fork 到 lmkd 杀进程,掌握了 ProcessRecord 的双值设计(curAdj/setAdj)、OOM Adj 的 19 级分级体系、以及 lmkd 基于 PSI 的五级压力杀进程策略。下一课将进入 **Activity 任务栈、启动模式与多窗口源码剖析**。
|
||||
|
||||
---
|
||||
|
||||
*Generated by AOSP Learning Assistant · Stage 5 Module A Lesson 1 · 2026-06-14*
|
||||
@@ -0,0 +1,919 @@
|
||||
# 阶段⑤ 模块A 第2课:Activity 任务栈、启动模式与多窗口源码剖析
|
||||
|
||||
> **课程定位**:阶段⑤ System Server 核心服务深度解析 · 模块A AMS深度 第2课
|
||||
> **难度等级**:⭐⭐⭐⭐⭐(深度源码级)
|
||||
> **预计阅读**:30~35 分钟
|
||||
> **前置课程**:阶段⑤第1课(AMS进程管理)、阶段③模块C第12课(AMS入门)
|
||||
> **源码基准**:AOSP 14.0.0_r50(兼容 Android 12/13/15 核心逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 课程目标
|
||||
|
||||
学完本课后,你将能够:
|
||||
|
||||
| 序号 | 能力目标 | 检验方式 |
|
||||
|:---:|:---------|:--------|
|
||||
| ① | 画出 WindowContainer → Task → ActivityRecord 的五层继承树 | 默写类图 |
|
||||
| ② | 解释四种 launchMode 在 `ActivityStarter` 中的源码级分支逻辑 | 画出流程图 |
|
||||
| ③ | 追踪 `startActivity()` 到 `realStartActivityLocked()` 的完整 IPC 调用链 | 写出10步链路 |
|
||||
| ④ | 理解 `FLAG_ACTIVITY_NEW_TASK` 与 `TaskAffinity` 的协作机制 | 给出3个场景 |
|
||||
| ⑤ | 掌握多窗口下 `TaskDisplayArea` 的任务栈隔离原理 | 画出分屏任务树 |
|
||||
| ⑥ | 使用 `dumpsys activity activities` 诊断任务栈状态 | 实战输出分析 |
|
||||
|
||||
---
|
||||
|
||||
## 📐 前置知识检查
|
||||
|
||||
在进入本课前,确认你已经掌握:
|
||||
|
||||
- [x] Activity 生命周期基础(onCreate → onResume → onPause → onStop → onDestroy)
|
||||
- [x] Binder IPC 基本调用链路(Client → Server → 回调)
|
||||
- [x] Intent 的基本结构(ComponentName / Action / Category / Flags)
|
||||
- [x] AMS 在 system_server 中的角色定位(第1课内容)
|
||||
- [x] `ActivityThread` 与 `ApplicationThread` 的关系
|
||||
- [ ] ⚠️ 如果不熟悉以上内容,请先复习阶段③模块B/C 对应课程
|
||||
|
||||
---
|
||||
|
||||
## 🧱 一、Task 系统全景架构:五层 WindowContainer 继承树
|
||||
|
||||
### 1.1 从 WindowContainer 到 ActivityRecord 的层级关系
|
||||
|
||||
Android 10 引入了 `WindowContainer` 基类,将所有窗口管理实体统一为一棵树。理解这棵树是理解 Task 系统的前提:
|
||||
|
||||
```
|
||||
WindowContainer (抽象基类 — 树节点)
|
||||
│
|
||||
├── RootWindowContainer (树根 — 整个系统的窗口容器)
|
||||
│ │ 单例,持有所有 Display 和顶层 Task
|
||||
│ │
|
||||
│ └── DisplayContent (每个屏幕一个)
|
||||
│ │ 代表物理/虚拟显示器
|
||||
│ │
|
||||
│ └── TaskDisplayArea (任务显示区域)
|
||||
│ │ 一个 Display 可以有多个 TDA(分屏时为2个)
|
||||
│ │ ★ 多窗口隔离的关键:每个分屏窗口对应一个 TDA ★
|
||||
│ │
|
||||
│ └── Task (任务栈)
|
||||
│ │ 一组 ActivityRecord 的集合
|
||||
│ │ 对应最近任务列表中的一张卡片
|
||||
│ │
|
||||
│ └── ActivityRecord (Activity 记录)
|
||||
│ 单个 Activity 实例的元数据
|
||||
│ 持有 Token、状态、Intent、进程引用
|
||||
```
|
||||
|
||||
### 1.2 WindowContainer 核心能力
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java
|
||||
|
||||
public class WindowContainer<E extends WindowContainer>
|
||||
implements Comparable<WindowContainer>, Animatable {
|
||||
|
||||
// === 树结构字段 ===
|
||||
protected WindowContainer mParent; // 父节点
|
||||
protected final ArraySet<E> mChildren = ...; // 子节点列表
|
||||
|
||||
// === 层级操作 ===
|
||||
void addChild(E child, int index); // 添加子节点
|
||||
void removeChild(E child); // 移除子节点
|
||||
void positionChildAt(int position, E child, boolean includingParents);
|
||||
void moveToFront(String reason); // 移到最前
|
||||
|
||||
// === 可见性控制 ===
|
||||
boolean mVisible; // 当前可见性
|
||||
void setVisible(boolean visible); // 递归设置可见性
|
||||
|
||||
// === Surface 控制 ===
|
||||
SurfaceControl mSurfaceControl; // SurfaceFlinger 图层
|
||||
SurfaceAnimator mSurfaceAnimator; // 动画控制
|
||||
|
||||
// === 遍历 ===
|
||||
void forAllWindows(Consumer<WindowState> callback, boolean traverseTopToBottom);
|
||||
}
|
||||
```
|
||||
|
||||
> **设计精髓**:所有窗口相关操作(添加/移除/显示/隐藏/动画)都在 `WindowContainer` 基类中实现,`Task`、`ActivityRecord`、`WindowState` 只需关注各自特有的逻辑。这种「组合模式」极大简化了窗口管理代码。
|
||||
|
||||
### 1.3 Task 数据结构深度解析
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/Task.java
|
||||
|
||||
public class Task extends WindowContainer<ActivityRecord> {
|
||||
|
||||
// ========== [身份标识] ==========
|
||||
int mTaskId; // 全局唯一 Task ID
|
||||
int mUserId; // 所属用户 ID(多用户)
|
||||
String affinity; // ★ TaskAffinity,决定归属
|
||||
Intent mLastNonFullscreenIntent; // 最后一次非全屏启动的 Intent
|
||||
|
||||
// ========== [状态标志] ==========
|
||||
boolean mIsFloating; // 是否浮动窗口(如气泡)
|
||||
boolean mDragResizing; // 是否正在拖拽调整大小
|
||||
boolean mOnTop; // 是否在最顶层
|
||||
int mResizeMode; // 调整大小模式
|
||||
|
||||
// ========== [结构关系] ==========
|
||||
TaskDisplayArea getTaskDisplayArea(); // 所属 TDA
|
||||
ActivityRecord getTopActivity(); // 栈顶 Activity
|
||||
ActivityRecord getRootActivity(); // 栈底 Activity(根)
|
||||
int getActivityCount(); // Activity 数量
|
||||
|
||||
// ========== [视觉属性] ==========
|
||||
int mBounds; // 任务边界(分屏时非全屏)
|
||||
boolean mMinimized; // 是否最小化(Android 12+)
|
||||
|
||||
// ========== [关键方法] ==========
|
||||
void addActivityToTop(ActivityRecord r); // 压入栈顶
|
||||
void removeActivity(ActivityRecord r); // 移除 Activity
|
||||
ActivityRecord getActivity(IBinder token); // 按 Token 查找
|
||||
void performClearTop(ActivityRecord r, ...); // 清栈到指定 Activity
|
||||
void performClearTask(); // 清空整个 Task
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 ActivityRecord 核心字段
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java
|
||||
|
||||
public class ActivityRecord extends WindowContainer<WindowState> {
|
||||
|
||||
// ========== [身份] ==========
|
||||
IBinder appToken; // ★ WMS 侧 Token,跨进程传递
|
||||
Task task; // 所属 Task
|
||||
ActivityInfo info; // AndroidManifest.xml 解析结果
|
||||
|
||||
// ========== [Intent] ==========
|
||||
Intent intent; // 启动该 Activity 的 Intent(可包含 flags)
|
||||
String resolvedType; // MIME 类型
|
||||
|
||||
// ========== [状态机] ==========
|
||||
enum State { INITIALIZING, STARTED, RESUMED, PAUSING, PAUSED,
|
||||
STOPPING, STOPPED, FINISHING, DESTROYING, DESTROYED, RESTARTING }
|
||||
State mState; // 当前生命周期状态
|
||||
|
||||
// ========== [进程关联] ==========
|
||||
ProcessRecord app; // 所在进程(null = 进程不存在)
|
||||
WindowProcessController mWindowProcessController; // Android 10+
|
||||
|
||||
// ========== [启动模式] ==========
|
||||
int launchMode; // 从 ActivityInfo 读取
|
||||
String taskAffinity; // 从 AndroidManifest 或默认包名
|
||||
|
||||
// ========== [配置] ==========
|
||||
Configuration configuration; // 屏幕配置(旋转/分屏时变化)
|
||||
boolean mIsExiting; // 是否正在退出
|
||||
|
||||
// ========== [窗口] ==========
|
||||
ActivityRecord mTaskOverlay; // 是否作为 Task 遮罩层
|
||||
boolean mShowForAllUsers; // 是否对所有用户可见
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 二、startActivity() 全链路追踪:10步跨越3进程
|
||||
|
||||
### 2.1 完整调用链路图
|
||||
|
||||
```
|
||||
用户调用 startActivity(intent)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ App 进程 (com.example.app) │
|
||||
│ │
|
||||
│ ① Activity.startActivity(intent) │
|
||||
│ └─ startActivityForResult(intent, -1) │
|
||||
│ └─ Instrumentation.execStartActivity() │
|
||||
│ └─ ActivityTaskManager.getService() │
|
||||
│ .startActivity(...) │
|
||||
│ └─ Binder IPC ─────────────────┐ │
|
||||
└───────────────────────────────────────────────────│───────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ system_server 进程 │
|
||||
│ │
|
||||
│ ② ActivityTaskManagerService.startActivity() │
|
||||
│ └─ startActivityAsUser(...) │
|
||||
│ │
|
||||
│ ③ ActivityStarter.executeRequest() ★ 核心入口 │
|
||||
│ │ │
|
||||
│ ├─ ④ 解析 Intent / 权限检查 / resolveActivity() │
|
||||
│ │ │
|
||||
│ ├─ ⑤ 确定目标 Task:computeStackFocus() │
|
||||
│ │ ├─ standard → getOrCreateTask() │
|
||||
│ │ ├─ singleTop → findTask() / 栈顶判断 │
|
||||
│ │ ├─ singleTask→ findTask() / clearTop() │
|
||||
│ │ └─ singleInstance → createNewTask() │
|
||||
│ │ │
|
||||
│ ├─ ⑥ 创建 ActivityRecord │
|
||||
│ │ │
|
||||
│ ├─ ⑦ 将 ActivityRecord 放入 Task │
|
||||
│ │ Task.addActivityToTop(activityRecord) │
|
||||
│ │ │
|
||||
│ └─ ⑧ resumeFocusedTasksTopActivities() │
|
||||
│ └─ RootWindowContainer.resumeFocusedTasks... │
|
||||
│ └─ Task.resumeTopActivityInnerLocked() │
|
||||
│ └─ ⑨ startSpecificActivity() │
|
||||
│ ├─ 进程存在?→ realStartActivity()│
|
||||
│ └─ 进程不存在?→ startProcess() │
|
||||
│ (回到第1课的进程创建链路) │
|
||||
│ │
|
||||
│ ⑩ realStartActivityLocked() │
|
||||
│ └─ app.thread.scheduleTransaction() ── Binder ─┐ │
|
||||
└──────────────────────────────────────────────────────│────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ App 进程 │
|
||||
│ │
|
||||
│ ApplicationThread.scheduleTransaction() │
|
||||
│ └─ ActivityThread.handleLaunchActivity() │
|
||||
│ └─ performLaunchActivity() │
|
||||
│ ├─ makeApplication() │
|
||||
│ ├─ Activity.attach() │
|
||||
│ └─ Activity.onCreate() ★ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 ActivityStarter:所有启动逻辑的「调度中心」
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
|
||||
|
||||
class ActivityStarter {
|
||||
|
||||
int executeRequest(Request request) {
|
||||
// === 第一阶段:合法性检查 ===
|
||||
// ① 检查调用者权限
|
||||
checkStartAnyActivityPermission(intent);
|
||||
// ② 解析 Activity 信息
|
||||
resolveActivity(intent, resolvedType, ...);
|
||||
// ③ 检查是否允许启动(后台限制等)
|
||||
checkBackgroundActivityStart(...);
|
||||
|
||||
// === 第二阶段:确定目标 Task ===
|
||||
// ④ 根据 launchMode 和 flags 找或创建 Task
|
||||
Task targetTask = computeStackFocus(activityInfo, intent, ...);
|
||||
|
||||
// === 第三阶段:处理复用逻辑 ===
|
||||
// ⑤ 如果是 singleTop / singleTask,检查是否复用
|
||||
ActivityRecord reused = findReusableActivity(targetTask, intent, ...);
|
||||
if (reused != null) {
|
||||
// onNewIntent 路径
|
||||
deliverNewIntent(reused, intent);
|
||||
return START_SUCCESS;
|
||||
}
|
||||
|
||||
// === 第四阶段:创建并放置 ActivityRecord ===
|
||||
// ⑥ new ActivityRecord
|
||||
ActivityRecord r = new ActivityRecord(..., intent, activityInfo, ...);
|
||||
// ⑦ 放入 Task
|
||||
targetTask.addActivityToTop(r);
|
||||
|
||||
// === 第五阶段:启动流程 ===
|
||||
// ⑧ Resume
|
||||
mRootWindowContainer.resumeFocusedTasksTopActivities(...);
|
||||
|
||||
return START_SUCCESS;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Task/TaskDisplayArea 的查找逻辑
|
||||
|
||||
```java
|
||||
// ActivityStarter.java → computeStackFocus() 简化逻辑
|
||||
|
||||
Task computeStackFocus(ActivityInfo aInfo, Intent intent, int launchMode) {
|
||||
|
||||
TaskDisplayArea targetTDA = getTaskDisplayArea(intent, activityOptions);
|
||||
// ── 多窗口时的 TDA 选择逻辑 ──
|
||||
// 如果指定了 displayId → 找到对应 TDA
|
||||
// 如果是分屏启动 → 选择用户指定的分屏 TDA
|
||||
// 否则 → 使用当前焦点的 TDA
|
||||
|
||||
String targetAffinity = aInfo.taskAffinity != null
|
||||
? aInfo.taskAffinity
|
||||
: aInfo.packageName;
|
||||
|
||||
switch (launchMode) {
|
||||
case LAUNCH_SINGLE_INSTANCE:
|
||||
// ★ 强制创建新 Task,不与任何现有 Task 共享
|
||||
return createNewTask(targetTDA, targetAffinity);
|
||||
|
||||
case LAUNCH_SINGLE_TASK:
|
||||
// ★ 在整个 TDA 中查找 affinity 匹配的 Task
|
||||
Task existing = targetTDA.findTaskByAffinity(targetAffinity);
|
||||
if (existing != null) {
|
||||
// 找到 → 将 existing 之上的 Activity 全部清掉 (clearTop)
|
||||
existing.performClearTop(existing.getRootActivity());
|
||||
return existing;
|
||||
}
|
||||
// 没找到 → 创建新 Task
|
||||
return createNewTask(targetTDA, targetAffinity);
|
||||
|
||||
case LAUNCH_SINGLE_TOP:
|
||||
// ★ 检查当前栈顶是否是目标 Activity
|
||||
Task topTask = targetTDA.getTopTask();
|
||||
if (topTask != null && topTask.getTopActivity().isSameIntent(intent)) {
|
||||
// 复用栈顶 → onNewIntent
|
||||
return topTask;
|
||||
}
|
||||
// 栈顶不匹配 → 走 standard 逻辑
|
||||
// (注意:是否需要 NEW_TASK 取决于调用者的 Task)
|
||||
return getOrCreateTask(targetTDA, intent);
|
||||
|
||||
case LAUNCH_MULTIPLE: // standard
|
||||
default:
|
||||
// ★ 默认行为:根据 FLAG_ACTIVITY_NEW_TASK 决定
|
||||
if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0) {
|
||||
return getOrCreateTask(targetTDA, targetAffinity);
|
||||
} else {
|
||||
// 复用调用者的 Task
|
||||
return callerActivity.getTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎭 三、四大启动模式:源码级行为对比
|
||||
|
||||
### 3.1 总览对比表
|
||||
|
||||
| 模式 | Manifest 常量 | Task 行为 | onNewIntent | 多实例 | 典型场景 |
|
||||
|:-----|:------------|:---------|:-----------|:------|:--------|
|
||||
| **standard** | `LAUNCH_MULTIPLE` | 放入调用者 Task | 不触发 | 允许 | 普通页面 |
|
||||
| **singleTop** | `LAUNCH_SINGLE_TOP` | 栈顶匹配则复用 | 触发 | 允许(不在栈顶时) | 搜索页、详情页 |
|
||||
| **singleTask** | `LAUNCH_SINGLE_TASK` | 独占 Task,clearTop | 触发 | 不允许(Task内唯一) | 主页、浏览器 |
|
||||
| **singleInstance** | `LAUNCH_SINGLE_INSTANCE` | 完全独占 Task | 触发 | 不允许(全局唯一) | 拨号、地图导航 |
|
||||
|
||||
### 3.2 每种模式的 Task 栈变化示意图
|
||||
|
||||
```
|
||||
场景:依次启动 A(standard), B(singleTop), C(singleTask), D(singleInstance)
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
TDA (TaskDisplayArea)
|
||||
═══════════════════════════════════════════════════════════
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
||||
│ Task #1 │ │ Task #2 │ │ Task #3 │
|
||||
│ affinity │ │ affinity │ │ affinity │
|
||||
│ =com.a │ │ =com.c │ │ =com.d │
|
||||
│ │ │ │ │ │
|
||||
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌────────┐ │
|
||||
│ │ A │ │ │ │ C │ │ │ │ D │ │
|
||||
│ │(std) │ │ │ │(sTsk)│ │ │ │(sInst) │ │
|
||||
│ └──────┘ │ │ └──────┘ │ │ └────────┘ │
|
||||
│ ┌──────┐ │ └──────────┘ └──────────────┘
|
||||
│ │ B │ │
|
||||
│ │(sTop)│ │
|
||||
│ └──────┘ │
|
||||
└──────────┘
|
||||
|
||||
关键观察:
|
||||
- A 和 B 在同一个 Task(B 有 affinity=com.a 或未设置,默认=包名)
|
||||
- C 有 affinity=com.c + singleTask → 独立 Task #2
|
||||
- D 有 singleInstance → 独占 Task #3,且该 Task 只能有 D 一个 Activity
|
||||
```
|
||||
|
||||
### 3.3 singleTask 的 clearTop 算法源码还原
|
||||
|
||||
```java
|
||||
// Task.java → performClearTop() 简化逻辑
|
||||
|
||||
void performClearTop(ActivityRecord target, String reason) {
|
||||
// target: 要被保留在栈顶的 Activity
|
||||
// 遍历栈中所有在 target 之上的 Activity
|
||||
|
||||
List<ActivityRecord> aboveActivities = new ArrayList<>();
|
||||
for (int i = mChildren.size() - 1; i >= 0; i--) {
|
||||
ActivityRecord r = mChildren.get(i);
|
||||
if (r == target) break; // 遇到 target,停止
|
||||
aboveActivities.add(r); // 收集 target 之上的 Activity
|
||||
}
|
||||
|
||||
// 依次 finish 掉这些 Activity
|
||||
for (ActivityRecord r : aboveActivities) {
|
||||
r.finishIfPossible(reason, false);
|
||||
removeActivity(r);
|
||||
}
|
||||
|
||||
// target Activity 收到 onNewIntent
|
||||
target.deliverNewIntent(newIntent);
|
||||
|
||||
// 将 target 移到栈顶
|
||||
positionChildAtTop(target);
|
||||
}
|
||||
```
|
||||
|
||||
**实例说明**:
|
||||
|
||||
```
|
||||
启动前:Task#1 → [A] → [B] → [C] → [D] (D是栈顶)
|
||||
调用 startActivity(singleTask-C)
|
||||
|
||||
执行 performClearTop(C):
|
||||
1. 找到 C 在栈中的位置 (index=2)
|
||||
2. 收集 C 之上的 Activity: [D]
|
||||
3. finish D
|
||||
4. C.deliverNewIntent()
|
||||
5. C 移到栈顶
|
||||
|
||||
启动后:Task#1 → [A] → [B] → [C] (C是栈顶,D已销毁)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ 四、Intent Flags:启动行为的「微调旋钮」
|
||||
|
||||
### 4.1 核心 Flags 一览
|
||||
|
||||
| Flag 常量 | 值 | 作用 | 与 launchMode 关系 |
|
||||
|:----------|:---|:-----|:------------------|
|
||||
| `FLAG_ACTIVITY_NEW_TASK` | 0x10000000 | 在新 Task 中启动 | ≈ singleTask(但不强制 clearTop) |
|
||||
| `FLAG_ACTIVITY_CLEAR_TOP` | 0x04000000 | 清除目标之上的 Activity | 常与 NEW_TASK 组合 |
|
||||
| `FLAG_ACTIVITY_SINGLE_TOP` | 0x20000000 | 栈顶匹配时复用 | ≈ singleTop |
|
||||
| `FLAG_ACTIVITY_CLEAR_TASK` | 0x00008000 | 先清空整个 Task | 必须与 NEW_TASK 组合 |
|
||||
| `FLAG_ACTIVITY_REORDER_TO_FRONT` | 0x00020000 | 将已有实例移到栈顶 | 不创建新实例 |
|
||||
| `FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS` | 0x00800000 | 不显示在最近任务 | Task 不出现在 Recents |
|
||||
|
||||
### 4.2 经典组合效果矩阵
|
||||
|
||||
```
|
||||
调用形式 │ Task 行为 │ Activity 行为
|
||||
───────────────────────────────────┼─────────────────────┼────────────────────
|
||||
NEW_TASK │ 新Task或匹配affinity │ 创建新实例(不一定clear top)
|
||||
NEW_TASK + CLEAR_TOP │ 匹配Task + clearTop │ 目标之上全清 + onNewIntent
|
||||
CLEAR_TASK + NEW_TASK │ 先清空再使用 │ 全新实例,旧的全部销毁
|
||||
SINGLE_TOP │ 复用调用者Task │ 栈顶匹配则onNewIntent
|
||||
REORDER_TO_FRONT │ 不改变Task结构 │ 直接移到栈顶(不recreate)
|
||||
NEW_TASK + SINGLE_TOP + CLEAR_TOP │ 匹配Task + clearTop │ 三合一效果
|
||||
```
|
||||
|
||||
### 4.3 FLAG_ACTIVITY_NEW_TASK + TaskAffinity 协作场景
|
||||
|
||||
```java
|
||||
// 场景:从 App-A 启动 App-B 的 Activity
|
||||
|
||||
// App-A (包名: com.app.a, 默认 affinity: com.app.a)
|
||||
// App-B (包名: com.app.b)
|
||||
|
||||
// === 场景 1: 不带 NEW_TASK ===
|
||||
Intent intent = new Intent(context, AppBActivity.class);
|
||||
startActivity(intent);
|
||||
// 结果:AppBActivity 被放入 App-A 的 Task 中!
|
||||
// Task#1(affinity=com.app.a) → [AppA-Act] → [AppB-Act]
|
||||
// 按返回键会回到 App-A
|
||||
|
||||
// === 场景 2: 带 NEW_TASK ===
|
||||
Intent intent = new Intent(context, AppBActivity.class);
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
// 结果:创建新 Task,affinity=com.app.b
|
||||
// Task#2(affinity=com.app.b) → [AppB-Act]
|
||||
// 两个 Task 独立存在于 Recents
|
||||
|
||||
// === 场景 3: AppB-Act 声明了 taskAffinity="com.app.b.custom" ===
|
||||
// Manifest: <activity android:name=".AppBActivity"
|
||||
// android:taskAffinity="com.app.b.custom"/>
|
||||
Intent intent = new Intent(context, AppBActivity.class);
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
// 结果:创建新 Task,affinity=com.app.b.custom (= 声明的值)
|
||||
// 下次再启动这个 Activity 时,会匹配到同一个 Task
|
||||
```
|
||||
|
||||
> **关键规则**:`TaskAffinity` 是 Task 的「护照」。`NEW_TASK` 告诉 AMS "我要出国(新Task)",`TaskAffinity` 告诉 AMS "我要去哪个国家(哪个Task)"。
|
||||
|
||||
---
|
||||
|
||||
## 🪟 五、多窗口/分屏下的任务栈管理
|
||||
|
||||
### 5.1 Android 12+ 的分屏架构
|
||||
|
||||
```
|
||||
RootWindowContainer
|
||||
│
|
||||
├── DisplayContent (主屏幕)
|
||||
│ │
|
||||
│ ├── TaskDisplayArea #1 (上半屏 / 左半屏)
|
||||
│ │ ├── Task #1 → [Activity-A1] → [Activity-A2]
|
||||
│ │ └── Task #2 → [Activity-A3]
|
||||
│ │
|
||||
│ └── TaskDisplayArea #2 (下半屏 / 右半屏)
|
||||
│ ├── Task #3 → [Activity-B1]
|
||||
│ └── Task #4 → [Activity-B2] → [Activity-B3]
|
||||
│
|
||||
└── DisplayContent (外接显示器)
|
||||
└── TaskDisplayArea #3 (全屏)
|
||||
└── Task #5 → [Activity-C1]
|
||||
```
|
||||
|
||||
> **核心理解**:每个分屏窗口对应一个 `TaskDisplayArea`,TDA 之间**任务栈完全隔离**。这意味着上半屏的 Task 和下半屏的 Task 互不干扰,各自拥有独立的返回栈。
|
||||
|
||||
### 5.2 分屏模式下的 startActivity 行为
|
||||
|
||||
```java
|
||||
// ActivityStarter.java → getTaskDisplayArea()
|
||||
|
||||
TaskDisplayArea getTaskDisplayArea(Intent intent, ActivityOptions options) {
|
||||
if (options != null && options.getLaunchDisplayId() != INVALID_DISPLAY) {
|
||||
// 明确指定了 Display → 找到对应 Display 的焦点 TDA
|
||||
DisplayContent dc = getDisplayContent(options.getLaunchDisplayId());
|
||||
return dc.getDefaultTaskDisplayArea();
|
||||
}
|
||||
|
||||
if (options != null && options.getLaunchTaskDisplayArea() != null) {
|
||||
// 指定了 TDA(如从分屏的某个窗口启动)
|
||||
return options.getLaunchTaskDisplayArea();
|
||||
}
|
||||
|
||||
if (inMultiWindowMode() && callerActivity != null) {
|
||||
// ★ 分屏时:默认将新 Activity 放入调用者所在的 TDA
|
||||
// 这是用户直觉所在——在哪个窗口操作,就在哪个窗口显示
|
||||
return callerActivity.getTask().getTaskDisplayArea();
|
||||
}
|
||||
|
||||
// 非分屏:使用默认 TDA(全屏的那个)
|
||||
return mRootWindowContainer.getDefaultTaskDisplayArea();
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Task 在 TDA 之间的移动
|
||||
|
||||
```java
|
||||
// Task.java → reparent()
|
||||
|
||||
void reparent(TaskDisplayArea newParent, boolean onTop, boolean animate) {
|
||||
// 场景1:用户将分屏窗口拖拽到全屏
|
||||
// Task 从一个 TDA 移动到另一个 TDA
|
||||
|
||||
TaskDisplayArea oldParent = getTaskDisplayArea();
|
||||
if (oldParent == newParent) return; // 同一 TDA,无需操作
|
||||
|
||||
// 从旧 TDA 移除
|
||||
oldParent.removeChild(this);
|
||||
|
||||
// 加入新 TDA
|
||||
if (onTop) {
|
||||
newParent.positionChildAtTop(this);
|
||||
} else {
|
||||
newParent.addChild(this);
|
||||
}
|
||||
|
||||
// ★ 注意:当前焦点 Activity 需要重新 Resume
|
||||
// 因为 TDA 切换意味着窗口焦点可能变化
|
||||
mRootWindowContainer.resumeFocusedTasksTopActivities();
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 分屏时的 Adj 计算差异
|
||||
|
||||
分屏模式下,两个 TDA 的 `TOP` Activity 都可能获得 `adj=0`:
|
||||
|
||||
```
|
||||
非分屏(全屏):
|
||||
TDA#1(全屏) → Task#1 → [TOP-Act] adj=0
|
||||
其他不可见 Activity adj=700~1001
|
||||
|
||||
分屏模式:
|
||||
TDA#1(上半屏) → Task#1 → [TOP-Act-A] adj=0 ← 两个TOP!
|
||||
TDA#2(下半屏) → Task#2 → [TOP-Act-B] adj=0 ← 两个TOP!
|
||||
|
||||
// 但实际只有一个是「焦点窗口」,另一个是「可见但非焦点」
|
||||
// curProcState 会区分:
|
||||
// 焦点窗口:PROCESS_STATE_TOP (2)
|
||||
// 非焦点可见窗口:PROCESS_STATE_TOP_SLEEPING (11)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 六、实战练习
|
||||
|
||||
### 练习 6.1:用 dumpsys 观察四种启动模式的 Task 变化
|
||||
|
||||
```bash
|
||||
# === 实验准备:清理环境 ===
|
||||
adb shell am force-stop com.android.chrome
|
||||
adb shell am force-stop com.android.calculator2
|
||||
|
||||
# === 步骤1:启动 Chrome(standard) ===
|
||||
adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
|
||||
# 查看任务栈
|
||||
adb shell dumpsys activity activities | grep -A30 "Display #0" | grep -E "Task{|ActivityRecord{"
|
||||
|
||||
# 预期输出:
|
||||
# Task{#1 type=standard U=0 visible=true ...
|
||||
# ActivityRecord{#1 u0 com.android.chrome/... t1}
|
||||
|
||||
# === 步骤2:观察 singleTask ===
|
||||
# Chrome 的 MainActivity 通常声明为 singleTask
|
||||
adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
|
||||
# 再次启动同一个 Activity → 不会创建新实例
|
||||
adb shell dumpsys activity activities | grep "com.android.chrome"
|
||||
# 输出只有 1 个实例 → onNewIntent 被触发
|
||||
|
||||
# === 步骤3:用 adb 模拟 NEW_TASK flag ===
|
||||
adb shell am start -f 0x10000000 -n com.android.calculator2/.Calculator
|
||||
# -f 0x10000000 = FLAG_ACTIVITY_NEW_TASK
|
||||
# 计算器会在自己的 Task 中启动
|
||||
|
||||
# === 步骤4:观察多个 Task ===
|
||||
adb shell dumpsys activity activities | grep -E "^\s+\* Task{"
|
||||
# 输出至少2个 Task(Chrome的 + 计算器的)
|
||||
```
|
||||
|
||||
### 练习 6.2:编写 Activity 验证 taskAffinity 作用
|
||||
|
||||
```java
|
||||
// === 在 AndroidManifest.xml 中声明 ===
|
||||
<activity android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="com.example.custom_affinity" />
|
||||
|
||||
<activity android:name=".SecondActivity"
|
||||
android:launchMode="standard" /> <!-- 默认 affinity=包名 -->
|
||||
|
||||
// === MainActivity.java ===
|
||||
public class MainActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle saved) {
|
||||
super.onCreate(saved);
|
||||
|
||||
// 打印当前 Task 信息
|
||||
Log.d("TASK_DEBUG", "Task ID: " + getTaskId());
|
||||
Log.d("TASK_DEBUG", "isTaskRoot: " + isTaskRoot());
|
||||
Log.d("TASK_DEBUG", "TaskAffinity: " +
|
||||
getPackageManager().getActivityInfo(getComponentName(), 0).taskAffinity);
|
||||
|
||||
// 启动 SecondActivity(不带 NEW_TASK)
|
||||
// 由于 SecondActivity 的 affinity=包名(默认),
|
||||
// 而 MainActivity 的 affinity=custom,
|
||||
// 不带 NEW_TASK → SecondActivity 进入 MAIN 的 Task? NO!
|
||||
//
|
||||
// 实际上:不带 NEW_TASK 时,新 Activity 总是进入调用者的 Task
|
||||
// taskAffinity 只在 NEW_TASK 时起作用!
|
||||
findViewById(R.id.btn).setOnClickListener(v -> {
|
||||
startActivity(new Intent(this, SecondActivity.class));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === SecondActivity.java ===
|
||||
public class SecondActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle saved) {
|
||||
super.onCreate(saved);
|
||||
Log.d("TASK_DEBUG", "SecondActivity - Task ID: " + getTaskId());
|
||||
// 如果从 MainActivity 不带 NEW_TASK 启动 → Task ID 与 MainActivity 相同
|
||||
// 如果带 NEW_TASK → 会创建新 Task(affinity=包名)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 运行后查看日志
|
||||
adb logcat -s TASK_DEBUG
|
||||
```
|
||||
|
||||
### 练习 6.3:用 dumpsys 分析分屏模式下的 TDA
|
||||
|
||||
```bash
|
||||
# === 步骤1:进入分屏 ===
|
||||
# 方式A:在设备上通过手势/最近任务进入分屏
|
||||
# 方式B:模拟分屏(Android 12+)
|
||||
adb shell am start --windowingMode 5 -n com.android.chrome/...
|
||||
|
||||
# === 步骤2:检查分屏状态 ===
|
||||
adb shell dumpsys activity activities | grep -E "Display|TaskDisplayArea|windowingMode"
|
||||
|
||||
# 预期输出:
|
||||
# Display #0 (activities from top to bottom):
|
||||
# TaskDisplayArea #1 (windowingMode=5) ← 分屏上半
|
||||
# * Task{#1 ...}
|
||||
# TaskDisplayArea #2 (windowingMode=5) ← 分屏下半
|
||||
# * Task{#2 ...}
|
||||
|
||||
# === 步骤3:观察两个 TDA 的 Task 栈 ===
|
||||
adb shell dumpsys activity activities | awk '/TaskDisplayArea/,/^$/' | head -40
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 七、课后习题
|
||||
|
||||
### 选择题
|
||||
|
||||
**Q1**:以下哪种情况会触发 `onNewIntent()` 而不是 `onCreate()`?
|
||||
|
||||
A. standard 模式,同一个 Activity 被启动两次
|
||||
B. singleTop 模式,目标 Activity 已在栈顶
|
||||
C. singleTask 模式,目标 Activity 不在任何 Task 中
|
||||
D. singleInstance 模式,第一次启动该 Activity
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**B**。singleTop 在栈顶匹配时触发 onNewIntent。A:standard 每次新建;C:不在 Task 中则新建;D:第一次必然新建。
|
||||
|
||||
**补充**:singleTask 模式下,如果目标 Activity 已在其 Task 中存在(即使不在栈顶)且使用了 CLEAR_TOP,也会触发 onNewIntent。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q2**:关于 `FLAG_ACTIVITY_NEW_TASK`,以下说法**错误**的是?
|
||||
|
||||
A. 加上此 flag 后一定会创建新的 Task
|
||||
B. 如果已有 affinity 匹配的 Task,会复用该 Task
|
||||
C. 通常应与 `FLAG_ACTIVITY_CLEAR_TOP` 配合使用
|
||||
D. 从非 Activity 的 Context(如 Service)启动 Activity 时必须使用
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**A**。NEW_TASK 不保证一定创建新 Task。如果有 affinity 匹配的已有 Task,会复用该 Task。选项 D 正确——非 Activity Context 启动 Activity 必须加 NEW_TASK,否则会抛异常。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q3**:分屏模式下,两个 TDA 各有一个前台 Activity。以下说法正确的是?
|
||||
|
||||
A. 两个 Activity 的 oom_score_adj 都是 0
|
||||
B. 只有一个 Activity 的 adj=0,另一个的 adj=-100(VISIBLE)
|
||||
C. 两个 Activity 的 adj 都是 -100
|
||||
D. 两个 Activity 的 curProcState 完全相同
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**A** 最接近真相。在 Android 的分屏实现中,两个可见的 TOP Activity 的 `curAdj` 通常都是 0(或非常接近 0),但 `curProcState` 不同:焦点窗口为 `PROCESS_STATE_TOP (2)`,非焦点可见窗口为 `PROCESS_STATE_TOP_SLEEPING (11)`。所以 D 错误。B 错误——可见非焦点 ≠ VISIBLE(-100),那是被完全遮挡但仍在前台 Task 中的情况。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 简答题
|
||||
|
||||
**Q4**:画出 `WindowContainer` → `RootWindowContainer` → `DisplayContent` → `TaskDisplayArea` → `Task` → `ActivityRecord` → `WindowState` 的完整继承树,并标注每个层级在分屏和非分屏场景下的实例数量。
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
|
||||
```
|
||||
非分屏场景:
|
||||
RootWindowContainer (1个,单例)
|
||||
└─ DisplayContent (1个,主屏)
|
||||
└─ TaskDisplayArea (1个,全屏)
|
||||
├─ Task #1 → [ActivityRecord #1] → [WindowState #1]
|
||||
├─ Task #2 → [ActivityRecord #2] → [WindowState #2]
|
||||
└─ Task #3 → [ActivityRecord #3] → [WindowState #3]
|
||||
|
||||
分屏场景:
|
||||
RootWindowContainer (1个,单例)
|
||||
└─ DisplayContent (1个,主屏)
|
||||
├─ TaskDisplayArea #1 (上/左 半屏)
|
||||
│ ├─ Task #1 → [ActivityRecord #1] → [WindowState #1]
|
||||
│ └─ Task #2 → [ActivityRecord #2] → [WindowState #2]
|
||||
└─ TaskDisplayArea #2 (下/右 半屏)
|
||||
└─ Task #3 → [ActivityRecord #3] → [WindowState #3]
|
||||
```
|
||||
|
||||
关键:非分屏时 TDA=1,分屏时 TDA=2。Task 永远隶属于某个 TDA,不能同时出现在两个 TDA 中。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q5**:解释 `singleTask` + `CLEAR_TOP` 与 `singleTask` 不带 `CLEAR_TOP` 的行为差异。给出具体的栈变化示例。
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
|
||||
```
|
||||
初始状态:Task#1 → [A] → [B] → [C] → [D] (D是栈顶,A是singleTask)
|
||||
|
||||
场景1:startActivity(A) — singleTask 不带 CLEAR_TOP
|
||||
① AMS 找到 Task#1(affinity匹配)
|
||||
② 发现 A 已在栈中(非栈顶)
|
||||
③ 执行 clearTop:finish 掉 B、C、D
|
||||
④ A 收到 onNewIntent
|
||||
⑤ A 移到栈顶
|
||||
结果:Task#1 → [A] ← singleTask 默认隐式带 clearTop!
|
||||
|
||||
场景2:startActivity(A) — singleTask 显式带 FLAG_ACTIVITY_CLEAR_TOP
|
||||
与场景1完全相同,因为 singleTask 本身隐含了 clearTop 行为
|
||||
|
||||
★ 关键认知:singleTask 在目标 Activity 已在栈中时,默认就会 clearTop,
|
||||
不需要额外加 FLAG_ACTIVITY_CLEAR_TOP。但如果目标不在栈中(新的),
|
||||
则 CLEAR_TOP 无意义——因为没有东西需要清。
|
||||
|
||||
场景3:singleTask 但 A 不在栈中
|
||||
初始状态:Task#1 → [X] → [Y]
|
||||
startActivity(A) — singleTask
|
||||
① AMS 查找 affinity 匹配的 Task → 找到 Task#1
|
||||
② A 不在 Task#1 中
|
||||
③ 创建新的 ActivityRecord A,放入 Task#1
|
||||
结果:Task#1 → [X] → [Y] → [A]
|
||||
注意:这次没有 clearTop!因为 A 不在栈中,直接新建。
|
||||
|
||||
场景4:singleTask 但目标在栈中,且不需要清栈
|
||||
// 这种情况在标准 singleTask 中不存在
|
||||
// 如果开发者想在 singleTask 的 Task 中保留栈,只能:
|
||||
// ① 不用 singleTask,改用 standard + 手动管理
|
||||
// ② 或者在 onNewIntent 中自行处理而不依赖系统 clearTop
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q6**:在分屏模式中,用户在「上半屏 Task-A」中点击按钮启动 Activity-X(standard 模式)。Activity-X 会被放入哪个 TDA?为什么?
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
|
||||
Activity-X 会被放入**上半屏的 TDA #1**。原因:
|
||||
|
||||
1. `ActivityStarter` 在确定 TDA 时,优先检查调用者 Activity 所在的 TDA
|
||||
2. 由于调用者(Task-A 中的按钮所在的 Activity)位于 TDA #1
|
||||
3. `standard` 模式且不带 `NEW_TASK` flag → 复用调用者的 Task
|
||||
4. 调用者的 Task 属于 TDA #1 → Activity-X 进入 TDA #1
|
||||
|
||||
如果要让 Activity-X 进入下半屏的 TDA #2,需要:
|
||||
- 使用 `ActivityOptions.setLaunchDisplayArea(TDA#2)` (非公开 API)
|
||||
- 或者使用 `Intent.FLAG_ACTIVITY_NEW_TASK` + `ActivityOptions.makeBasic().setLaunchDisplayId(...)`(Android 12+)
|
||||
|
||||
这体现了 **TDA 隔离原则**:默认情况下,启动操作不会跨越 TDA 边界。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 实战编程题
|
||||
|
||||
**Q7**:编写一个 App,包含 3 个 Activity(A、B、C),验证以下场景:
|
||||
|
||||
- A 声明为 `singleTask`,`taskAffinity="com.example.custom"`
|
||||
- B 声明为 `standard`(默认 affinity)
|
||||
- C 声明为 `singleInstance`
|
||||
|
||||
要求:
|
||||
1. 在 A 中启动 B,再在 B 中启动 C,使用 `dumpsys` 观察 Task 结构
|
||||
2. 在 C 中启动 A(带 `FLAG_ACTIVITY_NEW_TASK`),观察 A 回到自己的 Task 还是 C 的 Task
|
||||
3. 按下 Home 键后,用 `adb shell dumpsys activity activities` 观察三个 Task 的 `visible` 状态
|
||||
|
||||
提交:`dumpsys` 输出截图 + 注释说明每个 Task/AcitivtyRecord 的含义。
|
||||
|
||||
<details>
|
||||
<summary>预期结果提示</summary>
|
||||
|
||||
```
|
||||
初始:启动 A(singleTask, affinity=custom)
|
||||
Task#1(affinity=custom) → [A]
|
||||
|
||||
在 A 中启动 B(standard,无 NEW_TASK):
|
||||
Task#1(affinity=custom) → [A] → [B] ← B 进入 A 的 Task!
|
||||
|
||||
在 B 中启动 C(singleInstance):
|
||||
Task#1(affinity=custom) → [A] → [B]
|
||||
Task#2(affinity=包名) → [C] ← C 独占一个 Task
|
||||
|
||||
在 C 中启动 A(带 NEW_TASK):
|
||||
Task#1(affinity=custom) → [A] → [B] → [A_new?]
|
||||
等等——A 是 singleTask + affinity=custom,Task#1 匹配!
|
||||
Task#1(affinity=custom) → [A] ← clearTop!(B被清掉,A收到onNewIntent)
|
||||
Task#2(affinity=包名) → [C] ← 不受影响
|
||||
|
||||
按 Home 键后:
|
||||
两个 Task 都变为不可见(visible=false),但都不销毁
|
||||
dumpsys 中 visible=false,但 Activity 状态变为 STOPPED
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资料
|
||||
|
||||
| 资源 | 路径/链接 |
|
||||
|:-----|:--------|
|
||||
| ActivityStarter.java | `frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java` |
|
||||
| ActivityRecord.java | `frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java` |
|
||||
| Task.java | `frameworks/base/services/core/java/com/android/server/wm/Task.java` |
|
||||
| TaskDisplayArea.java | `frameworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java` |
|
||||
| RootWindowContainer.java | `frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java` |
|
||||
| WindowContainer.java | `frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java` |
|
||||
| ActivityStackSupervisor.java | `frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java` |
|
||||
| ActivityThread.java | `frameworks/base/core/java/android/app/ActivityThread.java` |
|
||||
| Android 任务与返回栈 | https://developer.android.com/guide/components/activities/tasks-and-back-stack |
|
||||
| Android 多窗口支持 | https://developer.android.com/guide/topics/large-screens/multi-window-support |
|
||||
|
||||
---
|
||||
|
||||
> **课程小结**:本课以 `WindowContainer` 五层继承树为起点,逐层剖析了 `Task` → `ActivityRecord` → `WindowState` 的数据结构与层级关系。完整追踪了 `startActivity()` 跨越三进程的 10 步调用链,重点分析了 `ActivityStarter` 中四种 launchMode 的分支逻辑和 `performClearTop` 的清栈算法。深入对比了 6 个核心 Intent Flags 的组合效果,解析了 `TaskDisplayArea` 在多窗口下的隔离机制。下一课将进入 **WMS 窗口管理:WindowState、SurfaceFlinger 与 VSync 渲染管线**。
|
||||
|
||||
---
|
||||
|
||||
*Generated by AOSP Learning Assistant · Stage 5 Module A Lesson 2 · 2026-06-14*
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
62
Makefile
Normal file
62
Makefile
Normal file
@@ -0,0 +1,62 @@
|
||||
# ========================================
|
||||
# Makefile — C 项目编译与运行
|
||||
# ========================================
|
||||
|
||||
# ---------- 编译器与选项 ----------
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -std=c11 -g
|
||||
LDFLAGS =
|
||||
LIBS =
|
||||
|
||||
# ---------- 项目结构 ----------
|
||||
SRCDIR = src
|
||||
INCDIR = include
|
||||
BUILDDIR= build
|
||||
BINDIR = bin
|
||||
|
||||
# ---------- 自动收集源文件 ----------
|
||||
SRCS = $(wildcard $(SRCDIR)/*.c)
|
||||
OBJS = $(SRCS:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
|
||||
TARGET = $(BINDIR)/app
|
||||
|
||||
# ---------- 默认目标 ----------
|
||||
.PHONY: all run clean help
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
# ---------- 链接 ----------
|
||||
$(TARGET): $(OBJS) | $(BINDIR)
|
||||
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
|
||||
|
||||
# ---------- 编译 ----------
|
||||
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) -I$(INCDIR) -c -o $@ $<
|
||||
|
||||
# ---------- 创建目录 ----------
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
$(BUILDDIR):
|
||||
mkdir -p $(BUILDDIR)
|
||||
|
||||
# ---------- 运行 ----------
|
||||
run: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
# ---------- 清理 ----------
|
||||
clean:
|
||||
rm -rf $(BUILDDIR) $(BINDIR)
|
||||
|
||||
# ---------- 帮助 ----------
|
||||
help:
|
||||
@echo "使用说明:"
|
||||
@echo " make — 编译项目"
|
||||
@echo " make run — 编译并运行"
|
||||
@echo " make clean — 清理构建产物"
|
||||
@echo " make help — 显示帮助"
|
||||
@echo ""
|
||||
@echo "项目结构:"
|
||||
@echo " src/ — 源文件 (.c)"
|
||||
@echo " include/ — 头文件 (.h)"
|
||||
@echo " build/ — 编译中间文件 (.o)"
|
||||
@echo " bin/ — 可执行文件"
|
||||
@@ -23,6 +23,9 @@ docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
| 文档 | 说明 | 适用对象 |
|
||||
|:-----|:------|:---------|
|
||||
| [📖 用户使用手册](./docs/user-manual.md) | 全功能操作指南:Agent / 工作流 / 知识库 / 模板市场 | 所有用户 |
|
||||
| [⭐ 最佳实践指南](./docs/best-practices.md) | Agent 设计、工作流模式、提示词工程 | 进阶用户 |
|
||||
| [❓ 常见问题 FAQ](./docs/faq.md) | 47 个常见问题与解答,覆盖全平台 | 所有用户 |
|
||||
| [📖 快速开始指南](./docs/quickstart.md) | 5 分钟快速上手:安装、配置、启动 | 新用户、开发者 |
|
||||
| [🏗️ 项目结构概览](./docs/project-structure.md) | 前端/后端目录结构与模块说明 | 开发者、维护者 |
|
||||
| [🏛️ 架构设计文档](./docs/architecture.md) | 系统架构图、技术选型、数据流说明 | 架构师、高级开发者 |
|
||||
|
||||
76
_check_agents.py
Normal file
76
_check_agents.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
BASE = "http://127.0.0.1:8037/api/v1"
|
||||
|
||||
# Login
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/auth/login",
|
||||
data=b"username=admin&password=123456",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
token = json.loads(urllib.request.urlopen(req).read())["access_token"]
|
||||
print(f"Token: {token[:50]}...")
|
||||
|
||||
# Get agents
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/agents?page_size=50",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
data = json.loads(urllib.request.urlopen(req).read())
|
||||
agents = data if isinstance(data, list) else data.get("data", [])
|
||||
print(f"\nTotal agents: {len(agents)}")
|
||||
|
||||
# Team members to check
|
||||
team_members = {
|
||||
"architect": "7ae1ace0-d2a6-4e55-855c-678489700b2b",
|
||||
"test_planner": "705811aa-32dd-44fc-ada6-069414ceb25e",
|
||||
"functional_tester": "d271d75f-f2c1-4b5c-94b5-0cdd0bc14d5a",
|
||||
"ux_reviewer": "75b295a7-3031-4e8c-bf61-8299e7e19b56",
|
||||
"edge_explorer": "f7305b12-fb1a-438c-a8fc-08dc5ce4e086",
|
||||
"performance_evaluator": "a3dde5d4-1ae3-4223-bed0-9c9fd7ababf8",
|
||||
}
|
||||
|
||||
for role, aid in team_members.items():
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/agents/{aid}",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
agent = json.loads(urllib.request.urlopen(req).read())
|
||||
if "error" in agent:
|
||||
print(f"\n[{role}] ERROR: {agent.get('message')}")
|
||||
|
||||
# Search by name
|
||||
matching = [a for a in agents if role in a.get("name","").lower() or role in a.get("description","").lower()]
|
||||
if matching:
|
||||
a = matching[0]
|
||||
print(f" Found by search: {a['name']} ({a['id'][:12]}...)")
|
||||
agent = a
|
||||
else:
|
||||
continue
|
||||
|
||||
name = agent.get("name", "?")
|
||||
wc = agent.get("workflow_config")
|
||||
nodes = wc.get("nodes", []) if wc else []
|
||||
llm = [n for n in nodes if n.get("type") == "llm"]
|
||||
if llm:
|
||||
nd = llm[0]["data"]
|
||||
tools = nd.get("selected_tools", [])
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[{role}] {name}")
|
||||
print(f" Model: {nd.get('model')} | Temp: {nd.get('temperature')} | MaxIter: {nd.get('max_iterations')}")
|
||||
print(f" Tools ({len(tools)}): {tools}")
|
||||
prompt = nd.get("prompt", "")
|
||||
print(f" Prompt length: {len(prompt)} chars")
|
||||
# Key instructions
|
||||
for kw in ["file_read", "grep", "scan", "directory", "源码", "Android"]:
|
||||
if kw.lower() in prompt.lower():
|
||||
idx = prompt.lower().find(kw.lower())
|
||||
snippet = prompt[max(0,idx-30):idx+100].replace('\n', ' ')[:130]
|
||||
print(f" [{kw}]: ...{snippet}...")
|
||||
else:
|
||||
print(f"\n[{role}] {name} - No LLM node found")
|
||||
print(f" workflow_config: {json.dumps(wc, ensure_ascii=False)[:300] if wc else 'NONE'}")
|
||||
except Exception as e:
|
||||
print(f"\n[{role}] Exception: {e}")
|
||||
396
_fix_agents.py
Normal file
396
_fix_agents.py
Normal file
@@ -0,0 +1,396 @@
|
||||
"""
|
||||
Fix all 6 agents in the 系统应用测试团队:
|
||||
1. Add missing tools (list_files, grep_search, project_scan, file_write)
|
||||
2. Update system prompts to mandate real source code scanning
|
||||
"""
|
||||
import urllib.request
|
||||
import json
|
||||
import copy
|
||||
|
||||
BASE = "http://127.0.0.1:8037/api/v1"
|
||||
|
||||
def login():
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/auth/login",
|
||||
data=b"username=admin&password=123456",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())["access_token"]
|
||||
|
||||
def get_agent(token, agent_id):
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/agents/{agent_id}",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
|
||||
def update_agent(token, agent_id, data):
|
||||
body = json.dumps(data).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/agents/{agent_id}",
|
||||
data=body,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method="PUT"
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
|
||||
# ---------- AGENT FIX DEFINITIONS ----------
|
||||
|
||||
# 1. ARCHITECT — needs list_files, project_scan, file_write
|
||||
ARCHITECT_ID = "7ae1ace0-d2a6-4e55-855c-678489700b2b"
|
||||
ARCHITECT_NEW_TOOLS = ["file_read", "grep_search", "web_search", "list_files", "project_scan", "file_write"]
|
||||
ARCHITECT_NEW_PROMPT = """You are a System Architect responsible for analyzing EXISTING codebases. You MUST scan real source code files — NEVER rely on design documents alone.
|
||||
|
||||
## MANDATORY FIRST STEPS:
|
||||
1. Use `list_files` to scan the target directory recursively (depth 3-4)
|
||||
2. Use `project_scan` to auto-identify project type, tech stack, and key source files
|
||||
3. Use `file_read` to read AT LEAST 8-10 core source files completely
|
||||
4. Use `grep_search` to find specific patterns (imports, annotations, architecture patterns)
|
||||
|
||||
## OUTPUT REQUIREMENTS:
|
||||
After thorough code scanning, produce an Architecture Context Document in Chinese:
|
||||
|
||||
### 技术栈全景
|
||||
- Language, framework versions, UI toolkit, DI, networking, database, build system
|
||||
- Based on ACTUAL build.gradle.kts / package.json / requirements.txt files read
|
||||
|
||||
### 目录结构与模块组织
|
||||
- Key directories and their roles
|
||||
- Based on ACTUAL directory listing
|
||||
|
||||
### 核心模块分析 (read and analyze these files)
|
||||
- Auth: how auth works (interceptor, token storage, refresh logic)
|
||||
- API/Network: how API calls are made (base URL, interceptors, SSE client)
|
||||
- Data/Local: database schema, DAOs, entities
|
||||
- UI: screen structure, component tree, navigation
|
||||
- ViewModel: state management, event handling, lifecycle
|
||||
|
||||
### 数据流路径
|
||||
- Login flow: LoginScreen → ViewModel → Repository → ApiService → TokenDataStore
|
||||
- SSE streaming: ChatScreen → ChatViewModel → ChatRepository → SseClient → OkHttp
|
||||
- Persistence: how messages/conversations are saved and restored
|
||||
|
||||
### 已知架构风险
|
||||
- Thread blocking (runBlocking on main/OkHttp threads)
|
||||
- Memory leaks (callbackFlow without lifecycle binding)
|
||||
- Missing error handling (try-catch gaps)
|
||||
- Database migration strategy
|
||||
|
||||
### 集成点与约束
|
||||
- Where new code should be integrated
|
||||
- Naming conventions, patterns to follow
|
||||
- What NOT to change
|
||||
|
||||
Be thorough and evidence-based. Every claim must be backed by a specific file path and line number you actually read."""
|
||||
|
||||
# 2. TEST PLANNER — needs list_files, grep_search, project_scan
|
||||
TEST_PLANNER_ID = "705811aa-32dd-44fc-ada6-069414ceb25e"
|
||||
TEST_PLANNER_NEW_TOOLS = ["task_plan", "file_write", "file_read", "web_search", "text_analyze", "list_files", "grep_search", "project_scan"]
|
||||
TEST_PLANNER_NEW_PROMPT = """You are a Test Planner. Based on the Architect's context document and your OWN code scanning, create a comprehensive test plan.
|
||||
|
||||
## MANDATORY: SCAN REAL CODE FIRST
|
||||
1. Use `list_files` to verify the project directory structure
|
||||
2. Use `project_scan` to identify all source files
|
||||
3. Use `file_read` to read key files identified by the architect
|
||||
4. Cross-reference the architect's claims against actual code
|
||||
|
||||
## TEST PLAN OUTPUT (JSON format):
|
||||
{
|
||||
"test_scope": {
|
||||
"modules": ["AuthInterceptor", "SseClient", "ChatViewModel", ...],
|
||||
"files_scanned": ["path/to/file1.kt", ...],
|
||||
"total_loc": 3000
|
||||
},
|
||||
"test_scenarios": [
|
||||
{
|
||||
"id": "TS-001",
|
||||
"category": "auth/login",
|
||||
"description": "...",
|
||||
"files_involved": ["AuthInterceptor.kt"],
|
||||
"risk_level": "critical|high|medium|low",
|
||||
"test_type": "functional|ux|boundary|performance"
|
||||
}
|
||||
],
|
||||
"risk_areas": [
|
||||
{"area": "...", "files": [...], "risk_score": 1-10, "rationale": "..."}
|
||||
],
|
||||
"user_profiles": [
|
||||
{"name": "新用户", "scenarios": [...]},
|
||||
{"name": "重度用户", "scenarios": [...]}
|
||||
]
|
||||
}
|
||||
|
||||
Output at least 20 test scenarios covering: auth flow, SSE streaming, message persistence, UI rendering, error recovery, network resilience, performance, accessibility."""
|
||||
|
||||
# 3. FUNCTIONAL TESTER — needs grep_search, list_files, project_scan
|
||||
FUNCTIONAL_TESTER_ID = "d271d75f-f2c1-4b5c-94b5-0cdd0bc14d5a"
|
||||
FUNCTIONAL_TESTER_NEW_TOOLS = ["browser_use", "http_request", "file_read", "file_write", "json_process", "list_files", "grep_search", "project_scan"]
|
||||
FUNCTIONAL_TESTER_NEW_PROMPT = """You are a Functional Tester / Bug Hunter. Your job is to find REAL bugs in REAL source code.
|
||||
|
||||
## CRITICAL: SCAN ACTUAL SOURCE CODE
|
||||
1. Use `list_files` to see all source files in the target directory
|
||||
2. Use `project_scan` to get an overview
|
||||
3. Use `file_read` to read EVERY core source file completely
|
||||
4. Use `grep_search` to find anti-patterns: "runBlocking", "!!", "Thread.sleep", "?.let", "GlobalScope"
|
||||
5. NEVER analyze design documents — only analyze actual .kt / .java / .xml files
|
||||
|
||||
## BUG HUNTING CHECKLIST:
|
||||
For each file, check:
|
||||
- [ ] Thread safety: any blocking calls on UI/OkHttp threads?
|
||||
- [ ] Coroutine lifecycle: are Jobs properly cancelled in onCleared()?
|
||||
- [ ] Error handling: try-catch for all parse/IO operations?
|
||||
- [ ] Null safety: any !! operators? Null checks before access?
|
||||
- [ ] Resource management: are OkHttp calls, MediaRecorder, flows properly released?
|
||||
- [ ] Race conditions: concurrent access to shared mutable state?
|
||||
- [ ] API contract: correct URL construction, proper error handling?
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
For each bug found, provide:
|
||||
```json
|
||||
{
|
||||
"bug_id": "BUG-XXX-NNN",
|
||||
"severity": "P0|P1|P2",
|
||||
"file_path": "exact path",
|
||||
"line_number": 42,
|
||||
"title": "one-line description",
|
||||
"current_code": "the problematic code snippet",
|
||||
"problem": "why this is a bug",
|
||||
"fix_code": "the corrected code",
|
||||
"reproduction": "how to trigger this bug"
|
||||
}
|
||||
```
|
||||
|
||||
Output AT LEAST 15 real bugs with concrete file paths and line numbers from actual code files you read."""
|
||||
|
||||
# 4. UX REVIEWER — needs grep_search, list_files
|
||||
UX_REVIEWER_ID = "75b295a7-3031-4e8c-bf61-8299e7e19b56"
|
||||
UX_REVIEWER_NEW_TOOLS = ["browser_use", "file_read", "file_write", "text_analyze", "list_files", "grep_search", "project_scan"]
|
||||
UX_REVIEWER_NEW_PROMPT = """You are a UX Reviewer / Interaction Designer. Compare the app against ChatGPT Android and 豆包 (Doubao).
|
||||
|
||||
## FIRST: READ THE REAL UI CODE
|
||||
1. Use `list_files` to find all screen/component files
|
||||
2. Use `file_read` to read: LoginScreen, ChatScreen, MessageBubble, AgentListScreen, SettingsScreen, VoiceInputButton
|
||||
3. Use `grep_search` for: "contentDescription", "semantics", "Modifier.clickable", "AnimatedVisibility"
|
||||
|
||||
## UX BENCHMARKING CHECKLIST (vs ChatGPT & 豆包):
|
||||
For each screen, evaluate:
|
||||
|
||||
### Login/Auth:
|
||||
- [ ] Registration flow exists? (ChatGPT: email→verify→profile)
|
||||
- [ ] Password visibility toggle?
|
||||
- [ ] Input validation with inline errors?
|
||||
- [ ] Loading state during login?
|
||||
|
||||
### Chat (core experience):
|
||||
- [ ] Streaming animation smoothness (ChatGPT: character-by-character)
|
||||
- [ ] Stop generation button?
|
||||
- [ ] Regenerate response?
|
||||
- [ ] Message feedback? (👍👎 like ChatGPT)
|
||||
- [ ] Copy message button?
|
||||
- [ ] Conversation history list?
|
||||
- [ ] Skeleton/shimmer loading?
|
||||
- [ ] Empty state guidance?
|
||||
- [ ] Error state with retry?
|
||||
|
||||
### Accessibility (Google Play requirement):
|
||||
- [ ] All icons have contentDescription?
|
||||
- [ ] TalkBack reads elements in logical order?
|
||||
- [ ] Touch targets >= 48dp?
|
||||
|
||||
### Visual Design:
|
||||
- [ ] Dark mode support?
|
||||
- [ ] Consistent spacing system?
|
||||
- [ ] Typography scale defined?
|
||||
- [ ] Animation/transitions present?
|
||||
|
||||
## OUTPUT:
|
||||
For each gap found, provide:
|
||||
- Gap description
|
||||
- What ChatGPT/豆包 does
|
||||
- Screens/file references
|
||||
- Priority (P0/P1/P2)
|
||||
- Estimated fix hours
|
||||
- Specific code fix suggestion"""
|
||||
|
||||
# 5. EDGE EXPLORER — needs grep_search, list_files
|
||||
EDGE_EXPLORER_ID = "f7305b12-fb1a-438c-a8fc-08dc5ce4e086"
|
||||
EDGE_EXPLORER_NEW_TOOLS = ["browser_use", "http_request", "code_execute", "file_write", "file_read", "regex_test", "list_files", "grep_search", "project_scan"]
|
||||
EDGE_EXPLORER_NEW_PROMPT = """You are an Edge Case Explorer. Find boundary conditions and exception paths in REAL source code.
|
||||
|
||||
## MANDATORY: SCAN REAL CODE
|
||||
1. Use `list_files` to find all source files
|
||||
2. Use `project_scan` for overview
|
||||
3. Use `file_read` to read every core file
|
||||
4. Use `grep_search` to find patterns like: "catch", "?.let", "?:", "if.*null", "requireNotNull", "!!"
|
||||
|
||||
## EDGE CASE DIMENSIONS TO EXPLORE:
|
||||
|
||||
### 1. Network Resilience
|
||||
- SSE disconnect → reconnection logic (exponential backoff? max retries?)
|
||||
- WiFi↔Mobile data switch → does the app recover?
|
||||
- Request timeout → proper error UI?
|
||||
- Token expiry during streaming → handled gracefully?
|
||||
|
||||
### 2. Concurrency
|
||||
- Rapid send button clicks → multiple SSE connections?
|
||||
- Agent switch during streaming → old stream cancelled?
|
||||
- Screen rotation during streaming → state preserved?
|
||||
- App background/foreground → SSE reconnection?
|
||||
|
||||
### 3. Data Integrity
|
||||
- Room migration → destructive fallback? User data loss?
|
||||
- Large messages (>100KB Markdown) → OOM?
|
||||
- Concurrent Room writes → race conditions?
|
||||
- Empty/null fields in API response → crash?
|
||||
|
||||
### 4. Input Boundaries
|
||||
- Empty message send
|
||||
- Extremely long message (>10000 chars)
|
||||
- Special characters / emoji / RTL text
|
||||
- SQL injection / XSS in message content
|
||||
|
||||
### 5. Resource Limits
|
||||
- Max messages per conversation
|
||||
- Memory usage with long conversations
|
||||
- Audio recording interruptions (phone call, etc.)
|
||||
- File upload size limits
|
||||
|
||||
## OUTPUT:
|
||||
For each edge case, provide:
|
||||
- Scenario ID and description
|
||||
- Code location (file:line)
|
||||
- Current behavior (from code analysis)
|
||||
- Expected behavior
|
||||
- Risk level (Critical/High/Medium/Low)
|
||||
- Fix recommendation with code snippet"""
|
||||
|
||||
# 6. PERFORMANCE EVALUATOR — needs grep_search, list_files
|
||||
PERFORMANCE_EVALUATOR_ID = "a3dde5d4-1ae3-4223-bed0-9c9fd7ababf8"
|
||||
PERFORMANCE_EVALUATOR_NEW_TOOLS = ["http_request", "browser_use", "file_write", "file_read", "json_process", "list_files", "grep_search", "project_scan"]
|
||||
PERFORMANCE_EVALUATOR_NEW_PROMPT = """You are a Performance & Memory Evaluator. Analyze REAL source code for performance anti-patterns and memory leaks.
|
||||
|
||||
## MANDATORY: SCAN REAL CODE
|
||||
1. Use `list_files` to find all source files
|
||||
2. Use `project_scan` for overview
|
||||
3. Use `file_read` to read every core file
|
||||
4. Use `grep_search` to find: "runBlocking", "Thread.sleep", "GlobalScope", "callbackFlow", "MutableStateFlow", "LazyColumn", "Recomposer"
|
||||
|
||||
## PERFORMANCE CHECKLIST:
|
||||
|
||||
### Thread Model:
|
||||
- [ ] Any runBlocking on main thread or OkHttp dispatcher? (ANR risk)
|
||||
- [ ] Thread.sleep on OkHttp callback threads? (blocks connection pool)
|
||||
- [ ] Proper use of Dispatchers (Main for UI, IO for disk/network, Default for CPU)?
|
||||
|
||||
### Memory:
|
||||
- [ ] callbackFlow with proper awaitClose? (connection leak)
|
||||
- [ ] ViewModel Jobs cancelled in onCleared()?
|
||||
- [ ] OkHttp Calls properly cancelled?
|
||||
- [ ] MediaRecorder/AudioPlayer released?
|
||||
- [ ] Bitmap/Coil memory management?
|
||||
- [ ] Room Flow subscriptions lifecycle-bound?
|
||||
|
||||
### Compose Recomposition:
|
||||
- [ ] LazyColumn with stable keys?
|
||||
- [ ] derivedStateOf for expensive calculations?
|
||||
- [ ] Modifier.composed vs Modifier construction cost?
|
||||
- [ ] SnapshotStateList vs MutableList + copy?
|
||||
- [ ] High-frequency state updates (SSE tokens) triggering full recomposition?
|
||||
|
||||
### Network Performance:
|
||||
- [ ] OkHttp connection pooling configured?
|
||||
- [ ] GZIP compression enabled?
|
||||
- [ ] DNS caching?
|
||||
- [ ] Image caching (Coil disk cache)?
|
||||
- [ ] SSE backpressure handling? (trySend return value check)
|
||||
|
||||
### Database:
|
||||
- [ ] Room queries on main thread?
|
||||
- [ ] Composite indexes on frequently queried fields?
|
||||
- [ ] Pagination for large datasets?
|
||||
- [ ] Transactions for batch operations?
|
||||
|
||||
## OUTPUT:
|
||||
For each issue, provide:
|
||||
- PERF-ID and severity (Critical/High/Medium/Low)
|
||||
- File path and line number
|
||||
- Anti-pattern description
|
||||
- Memory/CPU impact estimate
|
||||
- Fix code snippet
|
||||
- Expected improvement (e.g., "reduces recomposition from 600/min to 10/min")"""
|
||||
|
||||
# ---------- APPLY FIXES ----------
|
||||
|
||||
AGENTS_TO_FIX = [
|
||||
("architect", ARCHITECT_ID, ARCHITECT_NEW_TOOLS, ARCHITECT_NEW_PROMPT),
|
||||
("test_planner", TEST_PLANNER_ID, TEST_PLANNER_NEW_TOOLS, TEST_PLANNER_NEW_PROMPT),
|
||||
("functional_tester", FUNCTIONAL_TESTER_ID, FUNCTIONAL_TESTER_NEW_TOOLS, FUNCTIONAL_TESTER_NEW_PROMPT),
|
||||
("ux_reviewer", UX_REVIEWER_ID, UX_REVIEWER_NEW_TOOLS, UX_REVIEWER_NEW_PROMPT),
|
||||
("edge_explorer", EDGE_EXPLORER_ID, EDGE_EXPLORER_NEW_TOOLS, EDGE_EXPLORER_NEW_PROMPT),
|
||||
("performance_evaluator", PERFORMANCE_EVALUATOR_ID, PERFORMANCE_EVALUATOR_NEW_TOOLS, PERFORMANCE_EVALUATOR_NEW_PROMPT),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
token = login()
|
||||
print(f"Token obtained: {token[:30]}...")
|
||||
|
||||
for role, agent_id, new_tools, new_prompt in AGENTS_TO_FIX:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Fixing [{role}] {agent_id[:12]}...")
|
||||
|
||||
try:
|
||||
# Get current agent
|
||||
agent = get_agent(token, agent_id)
|
||||
if "error" in agent:
|
||||
print(f" ERROR getting agent: {agent.get('message')}")
|
||||
continue
|
||||
|
||||
name = agent.get("name", "?")
|
||||
print(f" Agent: {name}")
|
||||
|
||||
wc = agent.get("workflow_config", {})
|
||||
nodes = wc.get("nodes", [])
|
||||
llm_nodes = [n for n in nodes if n.get("type") == "llm"]
|
||||
|
||||
if not llm_nodes:
|
||||
print(f" WARNING: No LLM node found!")
|
||||
continue
|
||||
|
||||
old_tools = llm_nodes[0]["data"].get("selected_tools", [])
|
||||
print(f" Old tools: {old_tools}")
|
||||
print(f" New tools: {new_tools}")
|
||||
print(f" Added: {set(new_tools) - set(old_tools)}")
|
||||
print(f" Removed: {set(old_tools) - set(new_tools)}")
|
||||
|
||||
# Update the LLM node
|
||||
updated_wc = copy.deepcopy(wc)
|
||||
for node in updated_wc["nodes"]:
|
||||
if node.get("type") == "llm":
|
||||
node["data"]["selected_tools"] = new_tools
|
||||
node["data"]["prompt"] = new_prompt
|
||||
print(f" Prompt updated: {len(old_tools)} tools → {len(new_tools)} tools")
|
||||
print(f" New prompt length: {len(new_prompt)} chars")
|
||||
|
||||
# Prepare update payload
|
||||
update_data = {
|
||||
"name": agent.get("name"),
|
||||
"description": agent.get("description"),
|
||||
"workflow_config": updated_wc,
|
||||
"status": agent.get("status", "published"),
|
||||
}
|
||||
|
||||
# Send update
|
||||
result = update_agent(token, agent_id, update_data)
|
||||
if "error" in result:
|
||||
print(f" UPDATE FAILED: {result.get('message', result)}")
|
||||
else:
|
||||
print(f" ✅ UPDATE SUCCESSFUL")
|
||||
|
||||
except Exception as e:
|
||||
print(f" EXCEPTION: {e}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("All agent fixes applied!")
|
||||
397
_fix_round11.py
Normal file
397
_fix_round11.py
Normal file
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Fix Round 11: 解决根因 — 迭代次数/模型/收束指令
|
||||
1. MaxIter: architect 25, planner 30, others 25
|
||||
2. Model: architect flash→pro
|
||||
3. Prompts: add convergence/stop-scanning instructions
|
||||
"""
|
||||
import urllib.request
|
||||
import json
|
||||
import copy
|
||||
|
||||
BASE = "http://127.0.0.1:8037/api/v1"
|
||||
|
||||
def login():
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/auth/login",
|
||||
data=b"username=admin&password=123456",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())["access_token"]
|
||||
|
||||
def get_agent(token, agent_id):
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/agents/{agent_id}",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
|
||||
def update_agent(token, agent_id, data):
|
||||
body = json.dumps(data).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/agents/{agent_id}",
|
||||
data=body,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method="PUT"
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
|
||||
# ========== CONVERGENCE SUFFIX (appended to all prompts) ==========
|
||||
CONVERGENCE = """
|
||||
|
||||
## STOP-SCANNING RULE (最高优先级):
|
||||
- 你最多只能使用 5-7 次 file_read 读取核心文件
|
||||
- 当你已经读取了 5 个以上源文件后,必须立即开始撰写输出
|
||||
- 不要反复读取同一个文件
|
||||
- 不要追求完美——先产出再优化
|
||||
- 如果你的迭代次数超过 10 次,你必须在下一轮生成 file_write 输出"""
|
||||
|
||||
# ========== AGENT FIX DEFINITIONS ==========
|
||||
|
||||
# 1. ARCHITECT — MaxIter 10→25, flash→pro, add convergence
|
||||
ARCHITECT_ID = "7ae1ace0-d2a6-4e55-855c-678489700b2b"
|
||||
ARCHITECT_PROMPT = """You are a System Architect responsible for analyzing EXISTING codebases. You MUST scan real source code files — NEVER rely on design documents alone.
|
||||
|
||||
## MANDATORY FIRST STEPS (最多 7 次工具调用):
|
||||
1. Use `list_files` to scan the target directory recursively (depth 3-4)
|
||||
2. Use `project_scan` to auto-identify project type, tech stack, and key source files
|
||||
3. Use `file_read` to read the 5-7 MOST IMPORTANT source files (not all files)
|
||||
4. Use `grep_search` only for specific patterns you need to verify
|
||||
|
||||
## AFTER reading 5 files, YOU MUST START writing output. Do not keep reading.
|
||||
|
||||
## OUTPUT REQUIREMENTS:
|
||||
After thorough code scanning, produce an Architecture Context Document in Chinese:
|
||||
|
||||
### 技术栈全景
|
||||
- Language, framework versions, UI toolkit, DI, networking, database, build system
|
||||
- Based on ACTUAL build files and source code read
|
||||
|
||||
### 目录结构与模块组织
|
||||
- Key directories and their roles
|
||||
|
||||
### 核心模块分析 (focus on the 5-7 files you read)
|
||||
- Auth: how auth works (interceptor, token storage, refresh logic)
|
||||
- API/Network: how API calls are made (base URL, interceptors, SSE client)
|
||||
- Data/Local: database schema, DAOs, entities
|
||||
- UI: screen structure, component tree, navigation
|
||||
- ViewModel: state management, event handling, lifecycle
|
||||
|
||||
### 数据流路径 (trace through the code you read)
|
||||
- Login flow: LoginScreen > ViewModel > Repository > ApiService > TokenDataStore
|
||||
- SSE streaming: ChatScreen > ChatViewModel > ChatRepository > SseClient > OkHttp
|
||||
- Persistence: how messages/conversations are saved and restored
|
||||
|
||||
### 已知架构风险
|
||||
- Thread blocking (runBlocking on main/OkHttp threads)
|
||||
- Memory leaks (callbackFlow without lifecycle binding)
|
||||
- Missing error handling (try-catch gaps)
|
||||
- Database migration strategy
|
||||
|
||||
Be thorough but EFFICIENT. Every claim must be backed by a specific file path and line number.
|
||||
|
||||
IMPORTANT: Read 5-7 key files, then WRITE your output. Do not exceed 10 file_read calls.""" + CONVERGENCE
|
||||
|
||||
# 2. TEST PLANNER — MaxIter 18→30, add convergence
|
||||
TEST_PLANNER_ID = "705811aa-32dd-44fc-ada6-069414ceb25e"
|
||||
TEST_PLANNER_PROMPT = """You are a Test Planner. Based on the Architect's context document and your OWN code scanning, create a comprehensive test plan.
|
||||
|
||||
## MANDATORY: SCAN REAL CODE (最多 5 次 file_read)
|
||||
1. Use `list_files` to verify the project directory structure
|
||||
2. Use `project_scan` to identify all source files
|
||||
3. Use `file_read` to read the 3-5 most critical files identified by the architect
|
||||
4. Cross-reference the architect's claims against actual code
|
||||
5. AFTER reading 3-5 files, IMMEDIATELY write your test plan JSON — do not keep reading
|
||||
|
||||
## TEST PLAN OUTPUT (MUST be valid JSON, no other text):
|
||||
{
|
||||
"test_scope": {
|
||||
"modules": ["AuthInterceptor", "SseClient", "ChatViewModel", ...],
|
||||
"files_scanned": ["path/to/file1.kt", ...],
|
||||
"total_loc": 3000
|
||||
},
|
||||
"test_scenarios": [
|
||||
{
|
||||
"id": "TS-001",
|
||||
"category": "auth/login",
|
||||
"description": "...",
|
||||
"files_involved": ["AuthInterceptor.kt"],
|
||||
"risk_level": "critical|high|medium|low",
|
||||
"test_type": "functional|ux|boundary|performance"
|
||||
}
|
||||
],
|
||||
"risk_areas": [
|
||||
{"area": "...", "files": [...], "risk_score": 1-10, "rationale": "..."}
|
||||
],
|
||||
"user_profiles": [
|
||||
{"name": "新用户", "scenarios": [...]},
|
||||
{"name": "重度用户", "scenarios": [...]}
|
||||
]
|
||||
}
|
||||
|
||||
Output at least 15 test scenarios. Use Chinese. ONLY output JSON — no introduction, no conclusion.""" + CONVERGENCE
|
||||
|
||||
# 3. FUNCTIONAL TESTER — MaxIter 15→25
|
||||
FUNCTIONAL_TESTER_ID = "d271d75f-f2c1-4b5c-94b5-0cdd0bc14d5a"
|
||||
FUNCTIONAL_TESTER_PROMPT = """You are a Functional Tester / Bug Hunter. Your job is to find REAL bugs in REAL source code.
|
||||
|
||||
## CRITICAL: SCAN ACTUAL SOURCE CODE (最多 6 次 file_read)
|
||||
1. Use `list_files` to see all source files in the target directory
|
||||
2. Use `project_scan` to get an overview
|
||||
3. Use `file_read` to read the 5-6 most bug-prone core source files
|
||||
4. Use `grep_search` to find anti-patterns: "runBlocking", "!!", "Thread.sleep", "GlobalScope"
|
||||
5. AFTER reading 5-6 files, IMMEDIATELY write your bug report — do not keep reading
|
||||
|
||||
## BUG HUNTING CHECKLIST:
|
||||
For each file, check:
|
||||
- [ ] Thread safety: any blocking calls on UI/OkHttp threads?
|
||||
- [ ] Coroutine lifecycle: are Jobs properly cancelled in onCleared()?
|
||||
- [ ] Error handling: try-catch for all parse/IO operations?
|
||||
- [ ] Null safety: any !! operators? Null checks before access?
|
||||
- [ ] Resource management: are OkHttp calls, MediaRecorder, flows properly released?
|
||||
- [ ] Race conditions: concurrent access to shared mutable state?
|
||||
- [ ] API contract: correct URL construction, proper error handling?
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
For each bug found, provide:
|
||||
```json
|
||||
{
|
||||
"bug_id": "BUG-XXX-NNN",
|
||||
"severity": "P0|P1|P2",
|
||||
"file_path": "exact path",
|
||||
"line_number": 42,
|
||||
"title": "one-line description",
|
||||
"current_code": "the problematic code snippet",
|
||||
"problem": "why this is a bug",
|
||||
"fix_code": "the corrected code",
|
||||
"reproduction": "how to trigger this bug"
|
||||
}
|
||||
```
|
||||
|
||||
Output AT LEAST 12 real bugs with concrete file paths and line numbers.""" + CONVERGENCE
|
||||
|
||||
# 4. UX REVIEWER — MaxIter 12→25
|
||||
UX_REVIEWER_ID = "75b295a7-3031-4e8c-bf61-8299e7e19b56"
|
||||
UX_REVIEWER_PROMPT = """You are a UX Reviewer / Interaction Designer. Compare the app against ChatGPT Android and Doubao (豆包).
|
||||
|
||||
## FIRST: READ THE REAL UI CODE (最多 5 次 file_read)
|
||||
1. Use `list_files` to find all screen/component files
|
||||
2. Use `file_read` to read the 4-5 most important UI files: LoginScreen, ChatScreen, MessageBubble, ChatViewModel, VoiceInputButton
|
||||
3. Use `grep_search` for: "contentDescription", "semantics", "Modifier.clickable", "AnimatedVisibility"
|
||||
4. AFTER reading 4-5 files, IMMEDIATELY write your UX report
|
||||
|
||||
## UX BENCHMARKING CHECKLIST (vs ChatGPT & Doubao):
|
||||
|
||||
### Login/Auth:
|
||||
- Registration flow exists?
|
||||
- Password visibility toggle?
|
||||
- Input validation with inline errors?
|
||||
- Loading state during login?
|
||||
|
||||
### Chat (core experience):
|
||||
- Streaming animation smoothness (ChatGPT: character-by-character)
|
||||
- Stop generation button?
|
||||
- Regenerate response?
|
||||
- Message feedback? (like/dislike like ChatGPT)
|
||||
- Copy message button?
|
||||
- Conversation history list?
|
||||
- Skeleton/shimmer loading?
|
||||
- Empty state guidance?
|
||||
- Error state with retry?
|
||||
|
||||
### Accessibility (Google Play requirement):
|
||||
- All icons have contentDescription?
|
||||
- TalkBack reads elements in logical order?
|
||||
- Touch targets >= 48dp?
|
||||
|
||||
### Visual Design:
|
||||
- Dark mode support?
|
||||
- Consistent spacing system?
|
||||
- Typography scale defined?
|
||||
- Animation/transitions present?
|
||||
|
||||
## OUTPUT:
|
||||
For each gap found, provide:
|
||||
- Gap description
|
||||
- What ChatGPT/Doubao does
|
||||
- Screens/file references
|
||||
- Priority (P0/P1/P2)
|
||||
- Estimated fix hours
|
||||
- Specific code fix suggestion""" + CONVERGENCE
|
||||
|
||||
# 5. EDGE EXPLORER — MaxIter 15→25
|
||||
EDGE_EXPLORER_ID = "f7305b12-fb1a-438c-a8fc-08dc5ce4e086"
|
||||
EDGE_EXPLORER_PROMPT = """You are an Edge Case Explorer. Find boundary conditions and exception paths in REAL source code.
|
||||
|
||||
## MANDATORY: SCAN REAL CODE (最多 6 次 file_read)
|
||||
1. Use `list_files` to find all source files
|
||||
2. Use `project_scan` for overview
|
||||
3. Use `file_read` to read the 5-6 most important source files
|
||||
4. Use `grep_search` to find patterns like: "catch", "?.let", "?:", "if.*null", "requireNotNull", "!!"
|
||||
5. AFTER reading 5-6 files and running 2-3 grep searches, WRITE your edge case report
|
||||
|
||||
## EDGE CASE DIMENSIONS TO EXPLORE:
|
||||
|
||||
### 1. Network Resilience
|
||||
- SSE disconnect > reconnection logic (exponential backoff? max retries?)
|
||||
- WiFi/Mobile data switch > does the app recover?
|
||||
- Request timeout > proper error UI?
|
||||
- Token expiry during streaming > handled gracefully?
|
||||
|
||||
### 2. Concurrency
|
||||
- Rapid send button clicks > multiple SSE connections?
|
||||
- Agent switch during streaming > old stream cancelled?
|
||||
- Screen rotation during streaming > state preserved?
|
||||
- App background/foreground > SSE reconnection?
|
||||
|
||||
### 3. Data Integrity
|
||||
- Room migration > destructive fallback? User data loss?
|
||||
- Large messages (>100KB Markdown) > OOM?
|
||||
- Concurrent Room writes > race conditions?
|
||||
- Empty/null fields in API response > crash?
|
||||
|
||||
### 4. Input Boundaries
|
||||
- Empty message send / whitespace-only
|
||||
- Extremely long message (>10000 chars)
|
||||
- Special characters / emoji / RTL text
|
||||
- SQL injection / XSS in message content
|
||||
|
||||
### 5. Resource Limits
|
||||
- Max messages per conversation
|
||||
- Memory usage with long conversations
|
||||
- Audio recording interruptions (phone call, etc.)
|
||||
- File upload size limits
|
||||
|
||||
## OUTPUT:
|
||||
For each edge case, provide:
|
||||
- Scenario ID and description
|
||||
- Code location (file:line)
|
||||
- Current behavior (from code analysis)
|
||||
- Expected behavior
|
||||
- Risk level (Critical/High/Medium/Low)
|
||||
- Fix recommendation with code snippet""" + CONVERGENCE
|
||||
|
||||
# 6. PERFORMANCE EVALUATOR — MaxIter 15→25
|
||||
PERFORMANCE_EVALUATOR_ID = "a3dde5d4-1ae3-4223-bed0-9c9fd7ababf8"
|
||||
PERFORMANCE_EVALUATOR_PROMPT = """You are a Performance & Memory Evaluator. Analyze REAL source code for performance anti-patterns and memory leaks.
|
||||
|
||||
## MANDATORY: SCAN REAL CODE (最多 6 次 file_read)
|
||||
1. Use `list_files` to find all source files
|
||||
2. Use `project_scan` for overview
|
||||
3. Use `file_read` to read the 5-6 most performance-critical source files
|
||||
4. Use `grep_search` to find: "runBlocking", "Thread.sleep", "GlobalScope", "callbackFlow", "MutableStateFlow", "LazyColumn"
|
||||
5. AFTER reading 5-6 files, WRITE your performance report
|
||||
|
||||
## PERFORMANCE CHECKLIST:
|
||||
|
||||
### Thread Model:
|
||||
- Any runBlocking on main thread or OkHttp dispatcher? (ANR risk)
|
||||
- Thread.sleep on OkHttp callback threads? (blocks connection pool)
|
||||
- Proper use of Dispatchers (Main for UI, IO for disk/network, Default for CPU)?
|
||||
|
||||
### Memory:
|
||||
- callbackFlow with proper awaitClose? (connection leak)
|
||||
- ViewModel Jobs cancelled in onCleared()?
|
||||
- OkHttp Calls properly cancelled?
|
||||
- MediaRecorder/AudioPlayer released?
|
||||
- Bitmap/Coil memory management?
|
||||
- Room Flow subscriptions lifecycle-bound?
|
||||
|
||||
### Compose Recomposition:
|
||||
- LazyColumn with stable keys?
|
||||
- derivedStateOf for expensive calculations?
|
||||
- High-frequency state updates triggering full recomposition?
|
||||
|
||||
### Network Performance:
|
||||
- OkHttp connection pooling configured?
|
||||
- GZIP compression enabled?
|
||||
- DNS caching?
|
||||
- Image caching (Coil disk cache)?
|
||||
- SSE backpressure handling?
|
||||
|
||||
### Database:
|
||||
- Room queries on main thread?
|
||||
- Composite indexes on frequently queried fields?
|
||||
- Pagination for large datasets?
|
||||
- Transactions for batch operations?
|
||||
|
||||
## OUTPUT:
|
||||
For each issue, provide:
|
||||
- PERF-ID and severity (Critical/High/Medium/Low)
|
||||
- File path and line number
|
||||
- Anti-pattern description
|
||||
- Memory/CPU impact estimate
|
||||
- Fix code snippet""" + CONVERGENCE
|
||||
|
||||
# ---------- APPLY FIXES ----------
|
||||
|
||||
AGENTS_TO_FIX = [
|
||||
("architect", ARCHITECT_ID, ARCHITECT_PROMPT, 25, "deepseek-v4-pro"),
|
||||
("test_planner", TEST_PLANNER_ID, TEST_PLANNER_PROMPT, 30, "deepseek-v4-pro"),
|
||||
("functional_tester", FUNCTIONAL_TESTER_ID, FUNCTIONAL_TESTER_PROMPT, 25, "deepseek-v4-pro"),
|
||||
("ux_reviewer", UX_REVIEWER_ID, UX_REVIEWER_PROMPT, 25, "deepseek-v4-pro"),
|
||||
("edge_explorer", EDGE_EXPLORER_ID, EDGE_EXPLORER_PROMPT, 25, "deepseek-v4-pro"),
|
||||
("performance_evaluator", PERFORMANCE_EVALUATOR_ID, PERFORMANCE_EVALUATOR_PROMPT, 25, "deepseek-v4-pro"),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
token = login()
|
||||
print(f"Token obtained: {token[:30]}...")
|
||||
|
||||
for role, agent_id, new_prompt, max_iter, model in AGENTS_TO_FIX:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Fixing [{role}] {agent_id[:12]}...")
|
||||
|
||||
try:
|
||||
agent = get_agent(token, agent_id)
|
||||
if "error" in agent:
|
||||
print(f" ERROR getting agent: {agent.get('message')}")
|
||||
continue
|
||||
|
||||
name = agent.get("name", "?")
|
||||
print(f" Agent: {name}")
|
||||
|
||||
wc = agent.get("workflow_config", {})
|
||||
nodes = wc.get("nodes", [])
|
||||
llm_nodes = [n for n in nodes if n.get("type") == "llm"]
|
||||
|
||||
if not llm_nodes:
|
||||
print(f" WARNING: No LLM node found!")
|
||||
continue
|
||||
|
||||
old_model = llm_nodes[0]["data"].get("model", "?")
|
||||
old_iter = llm_nodes[0]["data"].get("max_iterations", "?")
|
||||
old_tools = llm_nodes[0]["data"].get("selected_tools", [])
|
||||
print(f" Model: {old_model} -> {model}")
|
||||
print(f" MaxIter: {old_iter} -> {max_iter}")
|
||||
|
||||
# Update the LLM node
|
||||
updated_wc = copy.deepcopy(wc)
|
||||
for node in updated_wc["nodes"]:
|
||||
if node.get("type") == "llm":
|
||||
node["data"]["model"] = model
|
||||
node["data"]["max_iterations"] = max_iter
|
||||
node["data"]["prompt"] = new_prompt
|
||||
print(f" Prompt updated: {len(new_prompt)} chars (was {len(llm_nodes[0]['data'].get('prompt',''))} chars)")
|
||||
|
||||
# Prepare update payload
|
||||
update_data = {
|
||||
"name": agent.get("name"),
|
||||
"description": agent.get("description"),
|
||||
"workflow_config": updated_wc,
|
||||
"status": agent.get("status", "published"),
|
||||
}
|
||||
|
||||
result = update_agent(token, agent_id, update_data)
|
||||
if "error" in result:
|
||||
print(f" UPDATE FAILED: {result.get('message', result)}")
|
||||
else:
|
||||
print(f" OK: MaxIter={max_iter} Model={model}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" EXCEPTION: {e}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("All Round 11 agent fixes applied!")
|
||||
1
_round10_output.txt
Normal file
1
_round10_output.txt
Normal file
@@ -0,0 +1 @@
|
||||
{"error":"VALIDATION_ERROR","message":"请求参数验证失败","details":[{"field":"body.71","message":"JSON decode error","type":"json_invalid"}]}
|
||||
10
_round10_sse.txt
Normal file
10
_round10_sse.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
data: {"type": "plan_start", "team_name": "系统应用测试团队", "member_count": 6, "project_path": "D:\\aaa\\aiagent\\team_projects\\5b032316-9e31-4ecb-a2b1-b46c7beff2eb\\Round-10-Test-Android-client-at-D-aaa-ai"}
|
||||
|
||||
data: {"type": "architect_start", "agent": "系统架构师", "paths": ["D:\\aaa\\aiagent\\android\\app\\src\\main\\java", "D:\\aaa\\aiagent\\android"]}
|
||||
|
||||
data: {"type": "plan", "status": "running", "agent": "测试规划师"}
|
||||
|
||||
data: {"type": "plan_done", "plan": {}, "raw_output": "已达最大迭代次数 (18),任务被截断", "success": false}
|
||||
|
||||
data: {"type": "error", "content": "test_planner 未能生成有效计划"}
|
||||
|
||||
123
_run_round10.py
Normal file
123
_run_round10.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Execute Round 10: Android Client Testing with improved virtual team.
|
||||
"""
|
||||
import urllib.request
|
||||
import json
|
||||
import time
|
||||
|
||||
BASE = "http://127.0.0.1:8037/api/v1"
|
||||
TEAM_ID = "5b032316-9e31-4ecb-a2b1-b46c7beff2eb"
|
||||
|
||||
def login():
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/auth/login",
|
||||
data=b"username=admin&password=123456",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())["access_token"]
|
||||
|
||||
TOKEN = login()
|
||||
print(f"Logged in. Token: {TOKEN[:40]}...")
|
||||
|
||||
# The task description
|
||||
TASK_DESCRIPTION = """## Round 10: Android Client Comprehensive Quality Audit
|
||||
|
||||
### Target
|
||||
D:\\aaa\\aiagent\\android - This is a Kotlin/Jetpack Compose Android client for the Tiangong AI Agent Platform.
|
||||
|
||||
### Mission
|
||||
Each team member MUST scan REAL source code files (NOT design documents). The target directory contains 40+ Kotlin source files. Use your tools to read them.
|
||||
|
||||
### MANDATORY FOR ALL AGENTS:
|
||||
1. FIRST: Use `project_scan` on D:\\aaa\\aiagent\\android to identify project type and key files
|
||||
2. SECOND: Use `list_files` with recursive=true to see all source files
|
||||
3. THIRD: Use `file_read` to read AT LEAST 5-8 core source files completely
|
||||
4. FOURTH: Use `grep_search` to find patterns across the codebase
|
||||
5. DO NOT analyze design documents (android-app-design.md) as a substitute for real code
|
||||
6. Every finding MUST cite specific file paths and line numbers from ACTUAL code you read
|
||||
|
||||
### Quality Standards:
|
||||
- Compare against ChatGPT Android app and Doubao (豆包) for UX benchmarking
|
||||
- Every bug must have: file path, line number, code snippet, fix suggestion
|
||||
- Cross-reference findings between phases to avoid duplicate counting
|
||||
- P0 = blocks release, P1 = serious, P2 = improvement
|
||||
|
||||
### Expected Deliverables per Phase:
|
||||
1. Architect: Architecture context doc based on real code scan (not design doc)
|
||||
2. Test Planner: 20+ test scenarios with risk assessment
|
||||
3. Functional Tester: 15+ real bugs with file:line evidence
|
||||
4. UX Reviewer: Benchmark vs ChatGPT/豆包 with gap analysis
|
||||
5. Edge Explorer: 40+ boundary scenarios from code analysis
|
||||
6. Performance Evaluator: Memory/CPU/thread anti-patterns from code
|
||||
|
||||
### Success Criteria:
|
||||
- ALL findings cite real source file paths and line numbers
|
||||
- NO findings based purely on design documents
|
||||
- Cross-phase duplication < 10%
|
||||
- At least 50% of findings independently verifiable by reading the cited code
|
||||
"""
|
||||
|
||||
print("\n=== Sending execution request ===")
|
||||
print(f"Task: {TASK_DESCRIPTION[:200]}...")
|
||||
|
||||
# Use streaming endpoint to monitor progress
|
||||
body = json.dumps({
|
||||
"project_description": TASK_DESCRIPTION
|
||||
}).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/teams/{TEAM_ID}/execute/stream",
|
||||
data=body,
|
||||
headers={
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
print("Connecting to SSE stream...")
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=600) # 10 min timeout
|
||||
# Read SSE events
|
||||
buffer = ""
|
||||
while True:
|
||||
chunk = response.read(4096)
|
||||
if not chunk:
|
||||
break
|
||||
try:
|
||||
text = chunk.decode("utf-8")
|
||||
except:
|
||||
text = chunk.decode("gbk", errors="replace")
|
||||
buffer += text
|
||||
# Process complete SSE events
|
||||
while "\n\n" in buffer:
|
||||
event_str, buffer = buffer.split("\n\n", 1)
|
||||
lines = event_str.strip().split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("data: "):
|
||||
try:
|
||||
data = json.loads(line[6:])
|
||||
event_type = data.get("type", "?")
|
||||
if event_type == "phase_start":
|
||||
print(f"\n[PHASE START] {data.get('name', '?')} - {data.get('role', '?')}")
|
||||
elif event_type == "phase_done":
|
||||
success = data.get("success", False)
|
||||
output_len = len(data.get("output", ""))
|
||||
print(f"[PHASE DONE] {data.get('name', '?')} - success={success}, output={output_len} chars")
|
||||
elif event_type == "final":
|
||||
print(f"\n[FINAL] Project: {data.get('project_path', '?')}")
|
||||
files = data.get("files", [])
|
||||
print(f"Files: {len(files)} total")
|
||||
for f in files[:10]:
|
||||
print(f" - {f}")
|
||||
if len(files) > 10:
|
||||
print(f" ... and {len(files)-10} more")
|
||||
elif event_type == "error":
|
||||
print(f"\n[ERROR] {data.get('content', '?')}")
|
||||
else:
|
||||
print(f"[{event_type}] ", end="", flush=True)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
print("\n=== Stream ended ===")
|
||||
except Exception as e:
|
||||
print(f"\nException: {e}")
|
||||
177
_run_round12.py
Normal file
177
_run_round12.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Round 12: Commander-monitored Android client testing
|
||||
Monitor phase transitions, detect failures, report to commander.
|
||||
Commander: Claude Code (you) monitors this output.
|
||||
"""
|
||||
import urllib.request
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
BASE = "http://127.0.0.1:8037/api/v1"
|
||||
TEAM_ID = "5b032316-9e31-4ecb-a2b1-b46c7beff2eb"
|
||||
|
||||
def login():
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/auth/login",
|
||||
data=b"username=admin&password=123456",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())["access_token"]
|
||||
|
||||
TOKEN = login()
|
||||
print(f"[CMD] Logged in. Token: {TOKEN[:30]}...")
|
||||
|
||||
TASK = """## 任务: Android客户端全面质量审计
|
||||
|
||||
### 目标
|
||||
D:\\aaa\\aiagent\\android - Kotlin/Jetpack Compose Android 客户端
|
||||
|
||||
### 要求
|
||||
1. 每个Agent必须扫描真实源代码文件(.kt/.xml/.gradle.kts),不得基于设计文档分析
|
||||
2. 对标ChatGPT Android和豆包(Doubao)进行UX体验对比
|
||||
3. 所有发现必须标注具体文件路径和行号
|
||||
4. 使用 list_files → project_scan → file_read → grep_search 标准流程
|
||||
|
||||
### 各阶段产出
|
||||
- 架构师: 技术栈分析、模块结构、数据流路径、已知架构风险
|
||||
- 测试规划师: 15+测试场景、风险矩阵、用户画像
|
||||
- 功能测试员: 12+真实bug(含文件路径+行号+修复代码)
|
||||
- UX审查员: vs ChatGPT/豆包的差距分析、优先级、修复建议
|
||||
- 边界探索员: 30+边界场景(网络/并发/数据/输入/资源)
|
||||
- 性能评估员: 线程模型/内存/Compose/网络/数据库反模式
|
||||
|
||||
### 成功标准
|
||||
- 所有发现引用真实源文件路径和行号
|
||||
- 跨阶段重复率<10%
|
||||
- 50%以上发现可独立验证
|
||||
"""
|
||||
|
||||
body = json.dumps({"project_description": TASK}).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/teams/{TEAM_ID}/execute/stream",
|
||||
data=body,
|
||||
headers={
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
print("[CMD] 任务已下达,等待虚拟团队执行...")
|
||||
print("[CMD] 监控中 — 发现问题会立即报告\n")
|
||||
|
||||
phases_seen = set()
|
||||
errors = []
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=900) # 15 min timeout
|
||||
buffer = ""
|
||||
while True:
|
||||
chunk = response.read(4096)
|
||||
if not chunk:
|
||||
break
|
||||
try:
|
||||
text = chunk.decode("utf-8")
|
||||
except:
|
||||
text = chunk.decode("gbk", errors="replace")
|
||||
buffer += text
|
||||
while "\n\n" in buffer:
|
||||
event_str, buffer = buffer.split("\n\n", 1)
|
||||
lines = event_str.strip().split("\n")
|
||||
for line in lines:
|
||||
if not line.startswith("data: "):
|
||||
continue
|
||||
try:
|
||||
data = json.loads(line[6:])
|
||||
event_type = data.get("type", "?")
|
||||
|
||||
if event_type == "plan_start":
|
||||
print(f"[{time.time()-start_time:.0f}s] PLAN_START | 团队: {data.get('team_name','?')} | 成员: {data.get('member_count','?')}")
|
||||
print(f"[{time.time()-start_time:.0f}s] 项目路径: {data.get('project_path','?')}")
|
||||
|
||||
elif event_type == "phase_start":
|
||||
pid = f"{data.get('phase','?')}-{data.get('name','?')}"
|
||||
print(f"\n[{time.time()-start_time:.0f}s] PHASE_START | 阶段{data.get('phase','?')}: {data.get('name','?')} | 角色: {data.get('role','?')} | Agent: {data.get('agent','?')}")
|
||||
|
||||
elif event_type == "phase_done":
|
||||
success = data.get("success", False)
|
||||
iters = data.get("iterations", "?")
|
||||
tool_calls = data.get("tool_calls", "?")
|
||||
output_len = len(data.get("output", ""))
|
||||
files = data.get("files", [])
|
||||
status_icon = "OK" if success else "FAIL"
|
||||
print(f"[{time.time()-start_time:.0f}s] PHASE_DONE {status_icon} | 阶段{data.get('phase','?')}: {data.get('name','?')}")
|
||||
print(f" success={success} iterations={iters} tool_calls={tool_calls} output={output_len}chars files={len(files)}")
|
||||
if not success:
|
||||
error_msg = data.get("error", "")
|
||||
print(f" ERROR: {error_msg[:300]}")
|
||||
errors.append({
|
||||
"phase": data.get("name", "?"),
|
||||
"role": data.get("role", "?"),
|
||||
"error": error_msg
|
||||
})
|
||||
if files:
|
||||
for f in files[:5]:
|
||||
print(f" file: {f}")
|
||||
|
||||
elif event_type == "plan_done":
|
||||
plan = data.get("plan", {})
|
||||
phases = plan.get("phases", [])
|
||||
success = data.get("success", False)
|
||||
print(f"\n[{time.time()-start_time:.0f}s] PLAN_DONE | success={success} | phases={len(phases)}")
|
||||
if not success:
|
||||
raw = data.get("raw_output", "")[:200]
|
||||
print(f" raw: {raw}")
|
||||
if phases:
|
||||
for p in phases:
|
||||
deps = p.get("depends_on", [])
|
||||
dep_str = f" depends_on={deps}" if deps else ""
|
||||
print(f" Phase{p.get('phase')}: {p.get('name','?')} [{p.get('role','?')}]{dep_str}")
|
||||
|
||||
elif event_type == "plan_fallback":
|
||||
print(f"\n[{time.time()-start_time:.0f}s] PLAN_FALLBACK | {data.get('content','?')[:200]}")
|
||||
|
||||
elif event_type == "qa_done":
|
||||
review = data.get("review", {})
|
||||
print(f"\n[{time.time()-start_time:.0f}s] QA_DONE | pass={review.get('pass','?')} score={review.get('score','?')}")
|
||||
|
||||
elif event_type == "final":
|
||||
print(f"\n[{'='*50}]")
|
||||
print(f"[{time.time()-start_time:.0f}s] FINAL | 执行完成!")
|
||||
print(f" 项目路径: {data.get('project_path','?')}")
|
||||
files = data.get("files", [])
|
||||
print(f" 产出文件: {len(files)} 个")
|
||||
for f in files[:15]:
|
||||
print(f" {f}")
|
||||
if len(files) > 15:
|
||||
print(f" ...还有 {len(files)-15} 个文件")
|
||||
deliv = data.get("deliverable", "")[:500]
|
||||
if deliv:
|
||||
print(f" 交付物摘要: {deliv}")
|
||||
|
||||
elif event_type == "error":
|
||||
msg = data.get("content", "")[:400]
|
||||
print(f"\n[{time.time()-start_time:.0f}s] *** ERROR *** {msg}")
|
||||
errors.append({"phase": "global", "error": msg})
|
||||
|
||||
else:
|
||||
# brief for other events
|
||||
short = str(data.get("content", str(data)))[:80]
|
||||
print(f"[{time.time()-start_time:.0f}s] {event_type}: {short}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
print(f"\n[CMD] 流结束 | 总耗时: {time.time()-start_time:.0f}s | 错误数: {len(errors)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[CMD] 连接异常: {e}")
|
||||
|
||||
finally:
|
||||
if errors:
|
||||
print(f"\n[CMD] === 需要处理的错误 ({len(errors)}) ===")
|
||||
for i, err in enumerate(errors):
|
||||
print(f" [{i+1}] {err['phase']}({err.get('role','?')}): {err['error'][:200]}")
|
||||
19
_start_api.ps1
Normal file
19
_start_api.ps1
Normal file
@@ -0,0 +1,19 @@
|
||||
$env:PYTHONPATH = 'D:\aaa\aiagent\backend'
|
||||
$proc = Start-Process -FilePath 'D:\aaa\aiagent\backend\venv\Scripts\python.exe' `
|
||||
-ArgumentList '-m', 'uvicorn', 'app.main:app', '--host', '0.0.0.0', '--port', '8037' `
|
||||
-WorkingDirectory 'D:\aaa\aiagent\backend' `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardOutput 'D:\aaa\aiagent\logs\api_stdout.log' `
|
||||
-RedirectStandardError 'D:\aaa\aiagent\logs\api_stderr.log' `
|
||||
-PassThru
|
||||
Write-Host "API PID: $($proc.Id)"
|
||||
Start-Sleep -Seconds 5
|
||||
$listening = netstat -ano | Select-String ":8037.*LISTENING"
|
||||
if ($listening) {
|
||||
Write-Host "SUCCESS: API is listening on port 8037"
|
||||
} else {
|
||||
Write-Host "WARNING: API not yet listening, checking stderr..."
|
||||
if (Test-Path 'D:\aaa\aiagent\logs\api_stderr.log') {
|
||||
Get-Content 'D:\aaa\aiagent\logs\api_stderr.log' -Tail 10
|
||||
}
|
||||
}
|
||||
4
_task_round9.json
Normal file
4
_task_round9.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"project_description": "Test Android client source code at D:/aaa/aiagent/android. Scan real .kt source files under app/src/main/java/. Check for bugs, UX issues vs ChatGPT/Doubao, edge cases, architecture performance, memory leaks. Provide fix suggestions with file paths and line numbers. Key files: ChatViewModel.kt, SseClient.kt, AuthInterceptor.kt, MessageDao.kt, ConversationDao.kt, ApiService.kt, all Screen composables in ui/chat/ and ui/login/. Tech stack: Jetpack Compose + MVVM + Hilt DI + Room + SSE + OkHttp.",
|
||||
"auto_approve_files": true
|
||||
}
|
||||
2194
agent_db_backup.sql
Normal file
2194
agent_db_backup.sql
Normal file
File diff suppressed because one or more lines are too long
2173
agent_db_dump.sql
Normal file
2173
agent_db_dump.sql
Normal file
File diff suppressed because one or more lines are too long
155
android/(红头)项目编译Debug版本和Release版本apk的方法.txt
Normal file
155
android/(红头)项目编译Debug版本和Release版本apk的方法.txt
Normal file
@@ -0,0 +1,155 @@
|
||||
# Android 项目编译 Debug 和 Release 版本 APK 的方法
|
||||
|
||||
## 项目信息
|
||||
- **项目名称**: com.xunpaisoft.social
|
||||
- **项目路径**: D:\zhini\android-im
|
||||
- **JDK 要求**: Java 11 或更高版本
|
||||
- **推荐 JDK**: Android Studio 内置 JDK 21
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 1. 检查 Java 版本
|
||||
```bash
|
||||
java -version
|
||||
```
|
||||
**注意**: 如果系统只有 Java 8,需要使用 Android Studio 的 JDK 21
|
||||
|
||||
### 2. 设置环境变量
|
||||
```powershell
|
||||
# 设置 Android Studio JDK 路径
|
||||
$env:JAVA_HOME="C:\Program Files\Android\Android Studio1\jbr"
|
||||
|
||||
# 设置 Android SDK 路径
|
||||
$env:ANDROID_HOME="C:\Users\$env:USERNAME\AppData\Local\Android\Sdk"
|
||||
```
|
||||
|
||||
## 编译方法
|
||||
|
||||
### 方法一:使用 Gradle Wrapper(推荐)
|
||||
|
||||
#### 1. Debug 版本编译
|
||||
```bash
|
||||
# 设置环境变量并编译 Debug 版本
|
||||
$env:JAVA_HOME="C:\Program Files\Android\Android Studio1\jbr"; $env:ANDROID_HOME="C:\Users\$env:USERNAME\AppData\Local\Android\Sdk"; .\gradlew.bat assembleDebug
|
||||
```
|
||||
|
||||
**编译结果**:
|
||||
- ✅ 编译状态: BUILD SUCCESSFUL
|
||||
- ⏱️ 编译时间: 约 34 秒
|
||||
- 📁 APK 位置: `app\build\outputs\apk\debug\app-debug.apk`
|
||||
|
||||
#### 2. Release 版本编译
|
||||
```bash
|
||||
# 设置环境变量并编译 Release 版本
|
||||
$env:JAVA_HOME="C:\Program Files\Android\Android Studio1\jbr"; $env:ANDROID_HOME="C:\Users\$env:USERNAME\AppData\Local\Android\Sdk"; .\gradlew.bat assembleRelease
|
||||
```
|
||||
|
||||
**编译结果**:
|
||||
- ✅ 编译状态: BUILD SUCCESSFUL
|
||||
- ⏱️ 编译时间: 约 3分48秒
|
||||
- 📁 APK 位置: `app\build\outputs\apk\release\app-release.apk`
|
||||
|
||||
### 方法二:使用 Android Studio(最简单)
|
||||
|
||||
1. **打开 Android Studio**
|
||||
2. **导入项目**: 选择 `D:\zhini\android-im` 目录
|
||||
3. **等待 Gradle 同步完成**
|
||||
4. **编译项目**:
|
||||
- **Debug 版本**: `Build` → `Build Bundle(s) / APK(s)` → `Build APK(s)`
|
||||
- **Release 版本**: `Build` → `Generate Signed Bundle / APK` → `APK`
|
||||
|
||||
## 安装 APK 到设备
|
||||
|
||||
### 安装 Debug 版本
|
||||
```bash
|
||||
adb install -r app\build\outputs\apk\debug\app-debug.apk
|
||||
```
|
||||
|
||||
### 安装 Release 版本
|
||||
```bash
|
||||
adb install -r app\build\outputs\apk\release\app-release.apk
|
||||
```
|
||||
|
||||
## 版本对比
|
||||
|
||||
| 特性 | Debug 版本 | Release 版本 |
|
||||
|------|------------|--------------|
|
||||
| **编译时间** | 约 34 秒 | 约 3分48秒 |
|
||||
| **APK 大小** | 较大 | 较小(优化后) |
|
||||
| **代码混淆** | 否 | 是(ProGuard) |
|
||||
| **调试信息** | 包含 | 移除 |
|
||||
| **性能** | 较慢 | 较快 |
|
||||
| **用途** | 开发测试 | 正式发布 |
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### 问题1: Java 版本不兼容
|
||||
**错误信息**: `Dependency requires at least JVM runtime version 11. This build uses a Java 8 JVM.`
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 停止现有 Gradle daemon
|
||||
.\gradlew.bat --stop
|
||||
|
||||
# 使用 Android Studio JDK 编译
|
||||
$env:JAVA_HOME="C:\Program Files\Android\Android Studio1\jbr"; .\gradlew.bat assembleDebug
|
||||
```
|
||||
|
||||
### 问题2: Gradle Wrapper 不存在
|
||||
**解决方案**: 项目已包含 `gradlew.bat` 文件,如果缺失可以重新创建。
|
||||
|
||||
### 问题3: 权限重复警告
|
||||
**现象**: 编译时出现权限重复警告
|
||||
**影响**: 不影响编译,但建议清理重复权限声明
|
||||
|
||||
## 编译命令总结
|
||||
|
||||
### 一键编译脚本
|
||||
```powershell
|
||||
# Debug 版本一键编译
|
||||
$env:JAVA_HOME="C:\Program Files\Android\Android Studio1\jbr"; $env:ANDROID_HOME="C:\Users\$env:USERNAME\AppData\Local\Android\Sdk"; .\gradlew.bat assembleDebug
|
||||
|
||||
# Release 版本一键编译
|
||||
$env:JAVA_HOME="C:\Program Files\Android\Android Studio1\jbr"; $env:ANDROID_HOME="C:\Users\$env:USERNAME\AppData\Local\Android\Sdk"; .\gradlew.bat assembleRelease
|
||||
```
|
||||
|
||||
### 清理和重新编译
|
||||
```bash
|
||||
# 清理项目
|
||||
.\gradlew.bat clean
|
||||
|
||||
# 重新编译 Debug 版本
|
||||
.\gradlew.bat assembleDebug
|
||||
|
||||
# 重新编译 Release 版本
|
||||
.\gradlew.bat assembleRelease
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **JDK 版本**: 必须使用 Java 11 或更高版本
|
||||
2. **环境变量**: 每次编译前需要设置正确的 JAVA_HOME
|
||||
3. **签名配置**: Release 版本需要配置签名文件
|
||||
4. **混淆规则**: Release 版本会应用 ProGuard 混淆规则
|
||||
5. **资源优化**: Release 版本会进行资源压缩和优化
|
||||
|
||||
## 文件位置
|
||||
|
||||
- **Debug APK**: `app\build\outputs\apk\debug\app-debug.apk`
|
||||
- **Release APK**: `app\build\outputs\apk\release\app-release.apk`
|
||||
- **Gradle Wrapper**: `gradlew.bat`
|
||||
- **构建配置**: `app\build.gradle`
|
||||
|
||||
## 成功标志
|
||||
|
||||
编译成功后会显示:
|
||||
```
|
||||
BUILD SUCCESSFUL in [时间]
|
||||
[数量] actionable tasks: [执行数量] executed, [跳过数量] up-to-date
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025年10月29日
|
||||
**适用版本**: Android Gradle Plugin 8.7.3
|
||||
**JDK 版本**: Android Studio JDK 21
|
||||
BIN
android/.gradle/8.9/checksums/checksums.lock
Normal file
BIN
android/.gradle/8.9/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
android/.gradle/8.9/checksums/md5-checksums.bin
Normal file
BIN
android/.gradle/8.9/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
android/.gradle/8.9/checksums/sha1-checksums.bin
Normal file
BIN
android/.gradle/8.9/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user